import * as I from 'infestines'
import * as R from 'ramda'

import * as PL from '../dist/partial.lenses.cjs'

import * as BST from './bst'
import * as Lambda from './lambda'
import * as T from './types'

let L = PL // L is a variable so we can override it for the tests.

const typedL = R.mapObjIndexed((val, key) => {
  const type = T[key]
  if (!I.isFunction(type)) throw Error(`Type of \`${key}\` missing`)
  return type(val)
}, PL)

const later = I.curry(
  (ms, v) => new Promise(resolve => setTimeout(() => resolve(v), ms))
)

//

const id = I.id
const X = L

function XYZ(x, y, z) {
  this.x = x
  this.y = y
  this.z = z
}

// Do not convert `XYZ` into an ES2015 class!
XYZ.prototype.norm = function norm() {
  return this.x * this.x + this.y * this.y + this.z * this.z
}

const a100000 = Array(100000).fill(1)

const Sum = {empty: () => 0, concat: (x, y) => x + y}

const numeric = f => x => (x !== undefined ? f(x) : undefined)
const offBy1 = L.iso(numeric(R.inc), numeric(R.dec))

const flatten = [
  L.optional,
  L.lazy(rec =>
    L.cond(
      [R.is(Array), [L.elems, rec]],
      [R.is(Object), [L.values, rec]],
      [L.identity]
    )
  )
]

const everywhere = [
  L.optional,
  L.lazy(rec => {
    const elems = L.seq([L.elems, rec], L.identity)
    const values = L.seq([L.values, rec], L.identity)
    return L.choose(x =>
      x instanceof Array ? elems : x instanceof Object ? values : L.identity
    )
  })
]

//

const Monad = ({of, chain}) => ({
  of,
  chain,
  ap: (x2yS, xS) => chain(x2y => chain(x => of(x2y(x)), xS), x2yS),
  map: (x2y, xS) => chain(x => of(x2y(x)), xS)
})

//

const MapConcatOf = Monoid =>
  Monad({
    of: x => [x, Monoid.empty()],
    chain: (x2yM, [x, sr]) => {
      const [y, sl] = x2yM(x)
      return [y, Monoid.concat(sl, sr)]
    }
  })

const Collect = {
  empty: () => Object.freeze([]),
  concat: (ls, rs) => Object.freeze([...rs, ...ls])
}

const CollectM = MapConcatOf(Collect)

const collectM = I.curry(
  (o, s) =>
    L.toFunction(o)(Object.freeze(s), undefined, CollectM, x => [x, [x]])[1]
)

//

const StateM = Monad({
  of: x => s => [x, s],
  chain: (x2yS, xS) => s1 => {
    const [x, s] = xS(s1)
    return x2yS(x)(s)
  }
})

const countS = x => x2n => {
  const n = (x2n[x] || 0) + 1
  return [n, L.set(`${x}`, n, x2n)]
}

//

function show(x) {
  switch (typeof x) {
    case 'string':
    case 'object':
      return JSON.stringify(x)
    default:
      return `${x}`
  }
}

const equals = (x, y) =>
  R.identical(x && Object.getPrototypeOf(x), y && Object.getPrototypeOf(y)) &&
  R.equals(x, y)

function toggleEnv() {
  process.env.NODE_ENV =
    process.env.NODE_ENV === 'production' ? 'development' : 'production'
}

const toExpr = f =>
  f
    .toString()
    .replace(/^\(\) => /, '')
    .replace(/\s+/g, ' ')
    .replace(/;\s*}/g, ' }')

function testEq(thunk, expect) {
  it(`${toExpr(thunk)} => ${show(expect)}`, async () => {
    const actual = await thunk()
    if (!equals(actual, expect))
      throw Error(`Expected: ${show(expect)}, actual: ${show(actual)}`)

    toggleEnv()
    try {
      const actual = await thunk()
      if (!equals(actual, expect))
        throw Error(`Expected: ${show(expect)}, actual: ${show(actual)}`)
    } finally {
      toggleEnv()
    }

    L = typedL
    try {
      const typed = await thunk()
      if (!equals(actual, typed))
        throw Error(`Typed: ${show(typed)}, actual: ${show(actual)}`)
    } finally {
      L = PL
    }
  })
}

const testThrows = thunk =>
  it(`${toExpr(thunk)} => throws`, async () => {
    let raised
    let result
    try {
      result = await thunk()
      raised = false
    } catch (e) {
      result = e
      raised = true
    }
    if (!raised)
      throw Error(
        `Expected ${toExpr(thunk)} to throw, returned ${show(result)}`
      )
  })

const empties = [undefined, null, false, true, '', 0, 0.0 / 0.0, {}, []]

describe('L.log', () => {
  testEq(() => L.set(L.log('label'), 'out', 'in'), 'out')
})

describe('L.getLog', () => {
  testEq(() => L.getLog(['x', 0, 'y'], {x: [{y: 101}]}), 101)
  testEq(
    () => L.getLog(['data', L.elems, 'y'], {data: [{x: 1}, {y: 2}, {y: 3}]}),
    2
  )
})

describe('L.compose', () => {
  testEq(() => L.get(L.compose(), 'any'), 'any')
  testEq(() => L.compose('x'), 'x')
  testEq(() => L.compose(101), 101)
  testEq(
    () =>
      L.compose(
        101,
        'x'
      ),
    [101, 'x']
  )
})

describe('L.identity', () => {
  testEq(() => L.get(L.identity, 'any'), 'any')
  testEq(() => L.modify(L.identity, R.add(1), 2), 3)
  testEq(() => L.modify([], R.add(1), 2), 3)
  testEq(() => L.remove(['x', L.identity], {x: 1, y: 2}), {y: 2})
})

describe('arities', () => {
  const arities = {
    FantasyFunctor: undefined,
    Identity: undefined,
    IdentityAsync: undefined,
    Select: undefined,
    add: 1,
    all1: 3,
    all: 3,
    alternatives: 1,
    and1: 2,
    and: 2,
    any: 3,
    append: 4,
    appendOp: 1,
    appendTo: 4,
    applyAt: 2,
    array: 1,
    arrays: 1,
    assign: 3,
    assignOp: 1,
    assignTo: 4,
    attemptEveryDown: 1,
    attemptEveryUp: 1,
    attemptSomeDown: 1,
    branch: 1,
    branchOr: 2,
    branches: 0,
    chain: 2,
    children: 4,
    choice: 0,
    choices: 1,
    choose: 1,
    collect: 2,
    collectAs: 3,
    collectTotal: 2,
    collectTotalAs: 3,
    complement: 4,
    compose: 0,
    concat: 3,
    concatAs: 4,
    cond: 0,
    condOf: 1,
    conjugate: 2,
    cons: 4,
    count: 2,
    countIf: 3,
    counts: 2,
    countsAs: 3,
    cross: 1,
    defaults: 1,
    define: 1,
    disjoint: 1,
    disperse: 3,
    divide: 1,
    dropPrefix: 1,
    dropSuffix: 1,
    elems: 4,
    elemsTotal: 4,
    entries: 4,
    filter: 1,
    find: 1,
    findWith: 1,
    first: undefined,
    flat: 0,
    flatten: 4,
    fold: 1,
    foldTraversalLens: 2,
    foldl: 4,
    foldr: 4,
    forEach: 3,
    forEachWith: 4,
    fromFantasy: 1,
    fromFantasyApplicative: 1,
    fromFantasyMonad: 1,
    get: 2,
    getAs: 3,
    getInverse: 2,
    getLog: 2,
    getter: 1,
    groupBy: 1,
    identity: 4,
    ifElse: 3,
    index: 1,
    indexed: 4,
    inverse: 1,
    is: 1,
    isDefined: 2,
    isEmpty: 2,
    iso: 2,
    iterate: 1,
    join: 3,
    joinAs: 4,
    joinIx: 1,
    json: 1,
    keyed: 4,
    keys: 4,
    keysEverywhere: 4,
    last: 4,
    lazy: 1,
    leafs: 4,
    lens: 2,
    limit: 2,
    log: 0,
    mapIx: 1,
    mapping: 1,
    mappings: 1,
    matches: 1,
    maximum: 2,
    maximumBy: 3,
    mean: 2,
    meanAs: 3,
    minimum: 2,
    minimumBy: 3,
    modify: 3,
    modifyAsync: 3,
    modifyOp: 1,
    multikeyed: 4,
    multiply: 1,
    negate: 4,
    none: 3,
    normalize: 1,
    offset: 2,
    optional: 4,
    or: 2,
    orAlternatively: 2,
    orElse: 2,
    partsOf: 1,
    pattern: 1,
    patterns: 1,
    pick: 1,
    pickIn: 1,
    pointer: 1,
    prefix: 1,
    prependOp: 1,
    prependTo: 4,
    product: 2,
    productAs: 3,
    prop: 1,
    props: 0,
    propsExcept: 0,
    propsOf: 1,
    query: 0,
    querystring: 4,
    reIx: 1,
    removable: 0,
    remove: 2,
    removeOp: 4,
    replace: 2,
    replaces: 2,
    required: 1,
    reread: 1,
    reverse: 4,
    rewrite: 1,
    satisfying: 1,
    seemsArrayLike: 1,
    select: 2,
    selectAs: 3,
    seq: 0,
    set: 3,
    setIx: 1,
    setOp: 1,
    setter: 1,
    singleton: 4,
    skipIx: 1,
    slice: 2,
    split: 1,
    subseq: 3,
    subset: 1,
    subtract: 1,
    suffix: 1,
    sum: 2,
    sumAs: 3,
    tieIx: 2,
    toFunction: 1,
    transform: 2,
    transformAsync: 2,
    traverse: 4,
    uncouple: 1,
    unfold: 1,
    ungroupBy: 1,
    unless: 1,
    unzipWith1: 1,
    uri: 4,
    uriComponent: 4,
    valueOr: 1,
    values: 4,
    when: 1,
    whereEq: 1,
    zero: 4,
    zipWith1: 1
  }

  for (const f in L) testEq(() => L[f].length, arities[f])
})

describe(`L.find`, () => {
  testEq(() => L.set(L.find(R.equals(2)), undefined, [2]), [])
  testEq(() => L.set(L.find(R.equals(2)))(undefined, [1, 2, 3]), [1, 3])
  testEq(() => L.set(L.find(R.equals(2)))(4)([1, 2, 3]), [1, 4, 3])
  testEq(() => L.set(L.find(R.equals(2)), 2)([1, 4, 3]), [1, 4, 3, 2])
  testEq(() => L.set(L.find(R.equals(2)), 2, undefined), [2])
  testEq(() => L.set(L.find(R.equals(2)), 2, []), [2])
  testEq(() => L.get(L.find(R.equals(2)), undefined), undefined)
  testEq(() => L.get(L.find(R.equals(2)), [3]), undefined)
  testEq(
    () => L.remove([L.rewrite(R.join('')), L.find(R.equals('A'))], 'LOLA'),
    'LOL'
  )
  testEq(
    () => L.set([L.rewrite(R.join('')), L.find(R.equals('O'))], 'A-', 'LOLA'),
    'LA-LA'
  )

  testEq(() => L.get(L.find(R.equals(1), {hint: 2}), [2, 2, 2, 1, 2]), 1)
  testEq(() => L.get(L.find(R.equals(1), {hint: 0}), [2, 2, 2, 1, 2]), 1)
  testEq(() => L.get(L.find(R.equals(1), {hint: 4}), [2, 1, 2, 2, 2]), 1)
  testEq(() => L.get(L.find(R.equals(1), {hint: 5}), 0), undefined)
  testEq(
    () =>
      L.get(
        L.find(
          R.pipe(
            Math.abs,
            R.equals(2)
          ),
          {hint: 2}
        ),
        [-1, -2, 3, 1, 2, 1]
      ),
    -2
  )
  testEq(() => L.get(L.find(R.equals(2), {hint: 10}), [3, 2, 1, 0]), 2)
  testEq(() => L.set(L.find(R.equals(2), {hint: 0}), 2, [0, 1]), [0, 1, 2])
})

describe(`L.get`, () => {
  testEq(() => L.get([], [[{x: {y: 101}}]]), [[{x: {y: 101}}]])
  testEq(
    () => L.get([0, L.findWith('x'), L.identity, 'y', []], [[{x: {y: 101}}]]),
    101
  )
  testEq(
    () => L.get([0, L.findWith('x'), [L.identity, 'y']], [[{x: {y: 101}}]]),
    101
  )
  testEq(
    () => L.get([[0, L.findWith('x')], [[L.identity], 'y']], [[{x: {y: 101}}]]),
    101
  )
  testEq(() => L.get(X.findWith('x', {hint: 1}), [{x: 1}, {x: 2}]), 2)
})

describe(`L.index`, () => {
  testEq(() => L.remove([L.rewrite(R.join('')), 1], 'lol'), 'll')
  testEq(() => L.modify(L.index(1), x => x + 1, [1, 2]), [1, 3])
  testEq(() => L.set([0], undefined, [null]), [])
  testEq(() => L.set([1], 4, [1, 2, 3]), [1, 4, 3])
  testEq(() => L.set(2, 4, undefined), [undefined, undefined, 4])
  testEq(() => L.set([2], 4, [1]), [1, undefined, 4])
  testEq(() => L.remove([0], [1, 2, 3]), [2, 3])
  testEq(() => L.set([1], undefined, [1, 2, 3]), [1, 3])
  testEq(() => L.set(2)(undefined, [1, 2, 3]), [1, 2])
  testEq(() => L.set([5], undefined, [1, 2, 3]), [1, 2, 3])
  testEq(() => L.get(5)(undefined), undefined)
  testEq(() => L.get([5], [1, 2, 3]), undefined)
  testEq(() => L.set(1, '2', ['1', '2', '3']), ['1', '2', '3'])
  empties.forEach(invalid => {
    testEq(() => L.get(0, invalid), undefined)
    testEq(() => L.set(0, 'f', invalid), ['f'])
  })
  testEq(
    () => L.set([L.rewrite(R.join('')), L.index(0)], 'Hello', 'x, world!'),
    'Hello, world!'
  )
  testEq(() => L.remove(0, []), [])
  testEq(() => L.remove(1, []), [])
})

describe(`L.prop`, () => {
  testEq(() => Object.keys(L.set('y', 1, {x: 2, z: 3})), ['x', 'z', 'y'])
  testEq(() => Object.keys(L.set('y', 1, {x: 2, y: 0, z: 3})), ['x', 'y', 'z'])
  testEq(() => Object.keys(L.remove('y', {z: 2, y: 0, x: 3})), ['z', 'x'])
  testEq(() => L.modify('x', x => x + 1, {x: 1}), {x: 2})
  testEq(() => L.set([L.prop('x')], undefined, {x: 1}), {})
  testEq(() => L.set(['x', L.required(null)], undefined, {x: 1}), {x: null})
  testEq(() => L.set(['x', L.required(null)], 2, {x: 1}), {x: 2})
  testEq(() => L.remove('y', {x: 1, y: 2}), {x: 1})
  testEq(() => L.set(['y'], 3, {x: 1, y: 2}), {x: 1, y: 3})
  testEq(() => L.set('z', 3, {x: 1, y: 2}), {x: 1, y: 2, z: 3})
  testEq(() => L.set(['z'], 3, undefined), {z: 3})
  testEq(() => L.get('z', undefined), undefined)
  testEq(() => L.get(['z'])({x: 1}), undefined)
  empties.forEach(invalid => {
    testEq(() => L.get('x', invalid), undefined)
    testEq(() => L.set('ex', true, invalid), {ex: true})
  })
  testEq(() => L.remove('x', {}), {})
})

describe('L.replace', () => {
  testEq(() => L.get(L.replace(undefined, ''), undefined), '')
  testEq(() => L.get(L.replace(undefined, ''), 'defined'), 'defined')
  testEq(() => L.set(L.replace(undefined, ''), '', 'anything'), undefined)
  testEq(
    () => L.set(L.replace(undefined, ''), 'defined', 'anything'),
    'defined'
  )
})

describe('L.defaults', () => {
  testEq(() => L.get(L.defaults(''), undefined), '')
  testEq(() => L.get(L.defaults(''), 'defined'), 'defined')
  testEq(() => L.set(L.defaults(''), '', 'anything'), undefined)
  testEq(() => L.set(L.defaults(''), 'defined', 'anything'), 'defined')
})

describe('L.define', () => {
  testEq(() => L.remove(L.define({x: 1}), {y: 0}), {x: 1})
  testEq(() => L.get(L.define([]), [1]), [1])
  testEq(() => L.get(['related', L.define([])], {}), [])
  testEq(() => L.set(L.define([]), undefined, undefined), [])
})

describe('L.valueOr', () => {
  for (const v of [0, false, true, '', [], {}]) {
    testEq(() => L.get(L.valueOr(1), v), v)
    testEq(() => L.set(L.valueOr(1), 1, v), 1)
  }
  for (const v of [null, undefined]) {
    testEq(() => L.get(L.valueOr(1), v), 1)
    testEq(() => L.set(L.valueOr(1), 1, v), 1)
  }
})

describe('L.normalize', () => {
  testEq(() => L.get(L.normalize(R.sortBy(I.id)), [1, 3, 2, 5]), [1, 2, 3, 5])
  testEq(
    () =>
      L.set([L.normalize(R.sortBy(I.id)), L.find(R.equals(2))], 4, [
        1,
        3,
        2,
        5
      ]),
    [1, 3, 4, 5]
  )
  testEq(
    () =>
      L.set([L.normalize(R.sortBy(I.id)), L.find(R.equals(2))], 4, undefined),
    [4]
  )
  testEq(
    () => L.remove([L.normalize(R.sortBy(I.id)), L.find(R.equals(2))], [2]),
    []
  )
  testEq(() => L.remove([L.normalize(R.sortBy(I.id))], [2]), undefined)
  testEq(
    () =>
      L.set([L.normalize(R.sortBy(I.id)), L.find(R.equals(2))], undefined, [
        1,
        3,
        2,
        5
      ]),
    [1, 3, 5]
  )
})

describe('L.rewrite', () => {
  testEq(() => L.get(L.rewrite(x => x - 1), 1), 1)
  testEq(() => L.get(L.rewrite(x => x - 1), undefined), undefined)
  testEq(() => L.set(L.rewrite(x => x - 1), undefined, 1), undefined)
  testEq(() => L.set(L.rewrite(x => x - 1), 3, 1), 2)
})

describe('L.reread', () => {
  testEq(() => L.get(L.reread(x => x - 1), 1), 0)
  testEq(() => L.get(L.reread(x => x - 1), undefined), undefined)
  testEq(() => L.set(L.reread(x => x - 1), undefined, 1), undefined)
  testEq(() => L.set(L.reread(x => x - 1), 3, 1), 3)
})

describe('L.getter', () => {
  testEq(() => L.get(['x', L.getter(R.pair)], {x: 101}), [101, 'x'])
})

describe('L.setter', () => {
  testEq(
    () => L.get([0, L.setter((x, y, i) => [x, y, i]), (x, i) => [x, i]], ['x']),
    ['x', 0]
  )
  testEq(() => L.set([0, L.setter((x, y, i) => [x, y, i])], 'y', ['x']), [
    ['y', 'x', 0]
  ])
})

describe('L.zero', () => {
  testEq(() => L.get(L.zero, 'anything'), undefined)
  testEq(() => L.get([L.zero, L.valueOr('whatever')], 'anything'), undefined)
  testEq(() => L.set(L.zero, 'anything', 'original'), 'original')
  testEq(() => L.collect([L.elems, L.zero], [1, 3]), [])
  testEq(() => L.remove([L.elems, L.zero], [1, 2]), [1, 2])
})

describe('composing with plain functions', () => {
  testEq(() => L.get(x => x + 1, 2), 3)
  testEq(() => L.modify(R.inc, R.negate, 1), 1)
  testEq(() => L.get(['x', (x, i) => [x, i]], {x: -1}), [-1, 'x'])
  testEq(() => L.collect([L.elems, (x, i) => [x, i]], ['x', 'y']), [
    ['x', 0],
    ['y', 1]
  ])
  testEq(() => L.collect([L.values, (x, i) => [x, i]], {x: 1, y: -1}), [
    [1, 'x'],
    [-1, 'y']
  ])
  testEq(() => L.get([0, (x, i) => [x, i]], [-1]), [-1, 0])
  testEq(() => L.get([0, 'x', R.negate], [{x: -1}]), 1)
  testEq(() => L.set([0, 'x', R.negate], 2, [{x: -1}]), [{x: -1}])
  testEq(() => L.get(I.always('always'), 'anything'), 'always')
  testEq(() => L.set(I.always('always'), 'anything', 'original'), 'original')
})

describe('L.chain', () => {
  testEq(
    () =>
      L.get(L.chain(elems => (I.isArray(elems) ? 0 : L.identity), 'elems'), {
        elems: ['x']
      }),
    'x'
  )
  testEq(
    () =>
      L.set(
        L.chain(elems => (I.isArray(elems) ? 0 : L.identity), 'elems'),
        'y',
        {elems: ['x']}
      ),
    {elems: ['y']}
  )
  testEq(
    () =>
      L.get(L.chain(elems => (I.isArray(elems) ? 0 : L.identity), 'elems'), {
        notit: true
      }),
    undefined
  )
  testEq(
    () =>
      L.set(
        L.chain(elems => (I.isArray(elems) ? 0 : L.identity), 'elems'),
        false,
        {notit: true}
      ),
    {notit: true}
  )
})

describe('L.orElse', () => {
  testEq(() => L.get(L.orElse('b', 'a'), {a: 2, b: 1}), 2)
  testEq(() => L.get(L.orElse('b', 'a'), {b: 2}), 2)
  testEq(() => L.set(L.orElse('b', 'a'), 3, {a: 2, b: 1}), {a: 3, b: 1})
  testEq(() => L.set(L.orElse('b', 'a'), 3, {b: 2}), {b: 3})
  testEq(() => L.modify(L.orElse(L.values, L.elems), R.inc, {x: 1, y: 2}), {
    x: 2,
    y: 3
  })
  testEq(() => L.modify(L.orElse(L.values, L.elems), R.inc, [2, 0, 3]), [
    3,
    1,
    4
  ])
  testEq(
    () =>
      L.collect(
        [
          L.elems,
          L.orElse(
            (x, i) => (i === 1 ? [1, x] : undefined),
            (x, i) => (i !== 1 ? [x, 0] : undefined)
          )
        ],
        ['a', 'b']
      ),
    [['a', 0], [1, 'b']]
  )
})

describe('L.choices', () => {
  testEq(() => L.get(L.choices('a'), {a: 2, b: 1}), 2)
  testEq(() => L.get(L.choices('a', 'b'), {a: 2, b: 1}), 2)
  testEq(() => L.get(L.choices('a', 'b'), {b: 2}), 2)
})

describe('L.choice', () => {
  testEq(() => L.get(L.choice('x', 'y'), {x: 'a'}), 'a')
  testEq(() => L.get(L.choice('x', 'y'), {y: 'b'}), 'b')
  testEq(() => L.get(L.choice('x', 'y'), {z: 'c'}), undefined)
  testEq(() => L.set(L.choice('x', 'y'), 'A', {x: 'a'}), {x: 'A'})
  testEq(() => L.set(L.choice('x', 'y'), 'B', {y: 'b'}), {y: 'B'})
  testEq(() => L.set(L.choice('x', 'y'), 'C', {z: 'c'}), {z: 'c'})
  testEq(() => L.modify(L.choice(L.elems, L.values), R.inc, {x: 1, y: 2}), {
    x: 2,
    y: 3
  })
  testEq(() => L.modify(L.choice(L.elems, L.values), R.inc, [2, 0, 3]), [
    3,
    1,
    4
  ])
})

describe('L.findWith', () => {
  testEq(() => L.get(L.findWith(['x', 1]), [{x: ['a']}, {x: ['b', 'c']}]), 'c')
  testEq(
    () => L.set(L.findWith(['x', 1]), 'd', [{x: ['a']}, {x: ['b', 'c']}]),
    [{x: ['a']}, {x: ['b', 'd']}]
  )
  testEq(() => L.remove(L.findWith(['x', 1]), [{x: ['a']}, {x: ['b', 'c']}]), [
    {x: ['a']},
    {x: ['b']}
  ])
  testEq(() => L.collect(L.findWith(L.elems), [1, [2], 3]), [2])
  testEq(
    () => L.get(L.findWith((x, i) => (i === 1 ? x : undefined)), ['a', 'b']),
    'b'
  )
})

describe('L.filter', () => {
  testEq(() => L.set(L.filter(R.T), [], undefined), [])
  testEq(() => L.set(L.filter(R.T), undefined, []), [])
  testEq(() => L.get(L.filter(R.lt(9)), [3, 1, 4, 1, 5, 9, 2]), [])
  testEq(() => L.get(L.filter(R.lt(2)), undefined), undefined)
  testEq(() => L.get(L.filter(R.lt(2)), [3, 1, 4, 1, 5, 9, 2]), [3, 4, 5, 9])
  testEq(() => L.remove([L.filter(R.lt(2)), 1], [3, 1, 4, 1, 5, 9, 2]), [
    3,
    5,
    9,
    1,
    1,
    2
  ])
  testEq(() => L.set(L.filter(R.lt(0)), [], [3, 1, 4, 1, 5, 9, 2]), [])
  testEq(() => L.remove(L.filter(R.lt(0)), [3, 1, 4, 1, 5, 9, 2]), [])
  testEq(() => L.remove(L.filter(R.lt(2)), [3, 1, 4, 1, 5, 9, 2]), [1, 1, 2])
  empties
    .filter(x => !I.isArray(x) && !I.isString(x))
    .forEach(invalid => {
      testEq(() => L.get(L.filter(I.always(true)), invalid), undefined)
      testEq(() => L.set(L.filter(I.always(true)), [1, '2', 3], invalid), [
        1,
        '2',
        3
      ])
    })
  testEq(() => L.remove(L.filter(c => 'a' <= c), 'JavaScript'), ['J', 'S'])
})

describe('L.slice', () => {
  testEq(() => L.get(L.slice(undefined, undefined), undefined), undefined)
  testEq(() => L.get(L.slice(undefined, undefined), 45), undefined)
  testEq(() => L.get(L.slice(undefined, undefined), []), [])
  testEq(() => L.get(L.slice(undefined, undefined), ''), [])
  testEq(() => L.set(L.slice(undefined, undefined), [], [101]), [])
  testEq(() => L.remove(L.slice(undefined, undefined), [101]), [])
  testEq(() => L.get(L.slice(4, 1), 'abcde'), [])
  testEq(
    () => L.set([L.rewrite(R.join('')), L.slice(4, 1)], 'xyz', 'abcde'),
    'abcdxyze'
  )
  testEq(() => L.set(L.slice(undefined, undefined), 'abba', 45), [
    'a',
    'b',
    'b',
    'a'
  ])
  testEq(
    () => L.set([L.rewrite(R.join('')), L.slice(-1, -1)], 'world', 'Hello, !'),
    'Hello, world!'
  )
  testEq(() => L.modify([L.slice(1, -1), L.elems], R.negate, [1, -2, -3, 4]), [
    1,
    2,
    3,
    4
  ])
  testEq(() => L.modify([L.slice(-3, 3), L.elems], R.negate, [1, -2, -3, 4]), [
    1,
    2,
    3,
    4
  ])
})

describe('L.prefix', () => {
  testEq(() => L.set(L.prefix(0), [1, 2], [3, 4]), [1, 2, 3, 4])
  testEq(() => L.set(L.prefix(), [1, 2], [3, 4]), [1, 2])
  testEq(() => L.set(L.prefix(Infinity), [], [3, 4]), [])
  testEq(() => L.remove(L.prefix(Infinity), [3, 4]), [])
  testEq(() => L.set(L.prefix(-1), [], [2, 3, 4]), [4])
})

describe('L.suffix', () => {
  testEq(() => L.set(L.suffix(0), [1, 2], [3, 4]), [3, 4, 1, 2])
  testEq(() => L.set(L.suffix(-1), [1, 2], [3, 4, 5]), [3, 1, 2])
  testEq(() => L.set(L.suffix(Infinity), [], [3, 4, 5]), [])
  testEq(() => L.remove(L.suffix(Infinity), [3, 4, 5]), [])
  testEq(() => L.set(L.suffix(), [1, 2], [3, 4, 5]), [1, 2])
})

describe('L.appendTo', () => {
  testEq(() => L.get([L.appendTo, (_, i) => i], 56), 0)
  testEq(() => L.get([L.appendTo, (_, i) => i], [11]), 1)
  testEq(() => L.get([L.appendTo, (_, i) => i], 'Hello'), 5)
  testEq(() => L.remove(L.appendTo, 45), [])
  testEq(
    () => L.remove([L.rewrite(R.join('')), L.appendTo], 'anything'),
    'anything'
  )
  empties.forEach(invalid => {
    testEq(() => L.set(L.appendTo, 'a', invalid), ['a'])
  })
  testEq(() => L.set(L.appendTo, 1, Int8Array.of(3, 1, 4)), [3, 1, 4, 1])
})

describe('L.prependTo', () => {
  testEq(() => L.get([L.prependTo, (_, i) => i], 56), 0)
  testEq(() => L.get([L.prependTo, (_, i) => i], [11]), 0)
  testEq(() => L.get([L.prependTo, (_, i) => i], 'Hello'), 0)
  testEq(() => L.remove(L.prependTo, 45), [])
  testEq(
    () => L.remove([L.rewrite(R.join('')), L.prependTo], 'anything'),
    'anything'
  )
  empties.forEach(invalid => {
    testEq(() => L.set(L.prependTo, 'a', invalid), ['a'])
  })
  testEq(() => L.set(L.prependTo, 3, Int8Array.of(1, 4, 1)), [3, 1, 4, 1])
})

describe('L.elemsTotal', () => {
  testEq(
    () =>
      L.modify([L.elemsTotal, L.when(I.isNumber)], R.negate, [1, undefined]),
    [-1, undefined]
  )
})

describe('L.elems', () => {
  testEq(() => L.modify(L.elems, R.identity, [0, NaN]), [0, NaN])
  testEq(() => L.modify(L.elems, R.identity, {x: 1, y: 2}), {x: 1, y: 2})
  testEq(() => L.modify(L.elems, R.inc, {x: 1, y: 2}), {x: 1, y: 2})
  testEq(() => L.modify(L.elems, R.negate, []), [])
  testEq(() => L.remove(L.elems, [1]), [])
  testEq(
    () =>
      L.modify(['xs', L.elems, 'x', L.elems], R.add(1), {
        xs: [{x: [1]}, {x: [2, 3, 4]}]
      }),
    {xs: [{x: [2]}, {x: [3, 4, 5]}]}
  )
  testEq(
    () =>
      L.set(['xs', L.elems, 'x', L.elems], 101, {
        xs: [{x: [1]}, {x: [2, 3, 4]}]
      }),
    {xs: [{x: [101]}, {x: [101, 101, 101]}]}
  )
  testEq(
    () =>
      L.remove(['xs', L.elems, 'x', L.elems], {
        ys: 'hip',
        xs: [{x: [1]}, {x: [2, 3, 4]}]
      }),
    {ys: 'hip', xs: [{x: []}, {x: []}]}
  )
  testEq(
    () =>
      L.modify(['xs', L.elems, 'x'], x => (x < 2 ? undefined : x), {
        xs: [{x: 3}, {x: 1}, {x: 4}, {x: 1, y: 0}, {x: 5}, {x: 9}, {x: 2}]
      }),
    {xs: [{x: 3}, {}, {x: 4}, {y: 0}, {x: 5}, {x: 9}, {x: 2}]}
  )
  testEq(
    () =>
      L.modify([L.elems, ['x', L.elems]], R.add(1), [
        {x: [1]},
        {},
        {x: []},
        {x: [2, 3]}
      ]),
    [{x: [2]}, {}, {x: []}, {x: [3, 4]}]
  )
  testEq(
    () =>
      L.modify([[L.elems, 'x'], L.elems], R.add(1), [
        {x: [1]},
        {y: 'keep'},
        {x: [], z: 'these'},
        {x: [2, 3]}
      ]),
    [{x: [2]}, {y: 'keep'}, {x: [], z: 'these'}, {x: [3, 4]}]
  )
})

describe('L.values', () => {
  testEq(() => L.modify(L.values, R.identity, [1, 2]), {'0': 1, '1': 2})
  testEq(() => L.modify(L.values, R.inc, [1, 2]), {'0': 2, '1': 3})
  testEq(() => L.modify(L.values, R.negate, {x: 11, y: 22}), {x: -11, y: -22})
  testEq(
    () =>
      L.remove([L.values, L.when(x => 11 < x && x < 33)], {
        x: 11,
        y: 22,
        z: 33
      }),
    {x: 11, z: 33}
  )
  testEq(() => L.remove(L.values, {x: 11, y: 22, z: 33}), {})
  testEq(() => L.modify(L.values, R.inc, {}), {})
  testEq(() => L.remove(L.values, {x: 1}), {})
  testEq(() => L.remove(L.values, null), null)
  testEq(() => L.modify(L.values, R.inc, null), null)
  testEq(() => L.modify(L.values, R.inc, 'anything'), 'anything')
  testEq(() => L.modify(L.values, R.inc, new XYZ(3, 1, 4)), {x: 4, y: 2, z: 5})
})

describe('L.optional', () => {
  testEq(() => L.collect(L.optional, undefined), [])
  testEq(() => L.collect(L.optional, 0), [0])
  testEq(() => L.collect([L.elems, L.elems], [[0, null], [false, NaN]]), [
    0,
    null,
    false,
    NaN
  ])
  testEq(
    () => L.collect([L.elems, 'x', L.optional], [{x: 1}, {y: 2}, {x: 3, z: 1}]),
    [1, 3]
  )
  testEq(
    () =>
      L.modify([L.elems, 'x', L.optional], R.add(1), [
        {x: 1},
        {y: 2},
        {x: 3, z: 1}
      ]),
    [{x: 2}, {y: 2}, {x: 4, z: 1}]
  )
  testEq(
    () =>
      L.collect(
        [L.elems, 'x', L.optional, L.elems],
        [{x: [1, 2]}, {y: 2}, {x: [3], z: 1}]
      ),
    [1, 2, 3]
  )
  testEq(
    () =>
      L.modify(
        [L.elems, 'x', L.optional, L.elems],
        x => (x < 2 ? undefined : x - 1),
        [{x: [1, 2]}, {y: 2}, {x: [3], z: 1}]
      ),
    [{x: [1]}, {y: 2}, {x: [2], z: 1}]
  )
})

describe('L.when', () => {
  testEq(() => L.get(L.when(x => x > 2), 1), undefined)
  testEq(() => L.get([L.when(x => x > 2), I.always(2)], 1), undefined)
  testEq(() => L.get(L.when(x => x > 2), 3), 3)
  testEq(() => L.collect([L.elems, L.when(x => x > 2)], [1, 3, 2, 4]), [3, 4])
  testEq(
    () => L.modify([L.elems, L.when(x => x > 2)], R.negate, [1, 3, 2, 4]),
    [1, -3, 2, -4]
  )
})

describe('L.unless', () => {
  testEq(() => L.get(L.unless(x => x <= 2), 1), undefined)
  testEq(() => L.get([L.unless(x => x <= 2), I.always(2)], 1), undefined)
  testEq(() => L.get(L.unless(x => x <= 2), 3), 3)
  testEq(() => L.collect([L.elems, L.unless(x => x <= 2)], [1, 3, 2, 4]), [
    3,
    4
  ])
  testEq(
    () => L.modify([L.elems, L.unless(x => x <= 2)], R.negate, [1, 3, 2, 4]),
    [1, -3, 2, -4]
  )
})

describe('L.collect', () => {
  testEq(
    () =>
      L.collect(['xs', L.elems, 'x', L.elems], {
        xs: [{x: [3, 1]}, {x: [4, 1]}, {x: [5, 9, 2]}]
      }),
    [3, 1, 4, 1, 5, 9, 2]
  )
  testEq(
    () =>
      L.collect([L.elems, 'x', L.elems], [{x: [1]}, {}, {x: []}, {x: [2, 3]}]),
    [1, 2, 3]
  )
  testEq(() => L.collect(L.elems, []), [])
  testEq(() => L.collect('x', {x: 101}), [101])
  testEq(() => L.collect('y', {x: 101}), [])
  testEq(
    () =>
      L.collect(['a', L.elems, 'b', L.elems, 'c', L.elems], {
        a: [{b: []}, {b: [{c: [1]}]}, {b: []}, {b: [{c: [2]}]}]
      }),
    [1, 2]
  )
  testEq(() => X.collect(X.elems, a100000).length, 100000)
})

describe('L.collectAs', () => {
  testEq(() => L.collectAs(R.negate, L.elems, [1, 2, 3]), [-1, -2, -3])
  testEq(
    () =>
      L.collectAs(x => (x < 0 ? undefined : x + 1), L.elems, [0, -1, 2, -3]),
    [1, 3]
  )
})

describe('L.concatAs', () => {
  testEq(() => L.concatAs(x => x + 1, Sum, L.elems, null), 0)
  testEq(() => L.concatAs(x => x + 1, Sum, [L.elems], []), 0)
  testEq(() => L.concatAs(x => x + 1, Sum, L.elems, [1, 2, 3]), 9)
  testEq(
    () =>
      L.concatAs(
        x => x + 1,
        Sum,
        [L.elems, 'x', L.optional],
        [{x: 1}, {y: 2}, {x: 3}]
      ),
    6
  )
})

describe('L.traverse', () => {
  testEq(
    () =>
      L.traverse(StateM, countS, flatten, [
        1,
        [[2, 1], 1],
        2,
        [3, [[4]], [3, 4]],
        5
      ])({}),
    [[1, [[1, 2], 3], 2, [1, [[1]], [2, 2]], 1], {1: 3, 2: 2, 3: 2, 4: 2, 5: 1}]
  )
})

describe('folds', () => {
  testEq(() => L.isDefined(L.elems, []), false)
  testEq(() => L.isDefined(L.elems, [1]), true)
  testEq(() => L.isDefined('x', {y: 1}), false)
  testEq(() => L.isDefined([L.elems, 'x'], [{}]), false)
  testEq(() => L.isDefined([L.elems, 'x', L.optional], [{}]), false)
  testEq(() => L.isEmpty(L.elems, []), true)
  testEq(() => L.isEmpty(L.elems, [1]), false)
  testEq(() => L.isEmpty([L.elems, 'x'], [{}]), false)
  testEq(() => L.isEmpty([L.elems, 'x', L.optional], [{}]), true)
  testEq(() => X.concat(Sum, X.elems, a100000), 100000)
  testEq(() => X.concatAs(id, Sum, X.elems, a100000), 100000)
  testEq(() => L.maximum([L.elems, 'x'], []), undefined)
  testEq(() => L.minimum([L.elems, 'x'], []), undefined)
  testEq(() => L.maximum(L.elems, 'JavaScript'), 'v')
  testEq(() => L.maximumBy(R.negate, L.elems, [1, 2, 3]), 1)
  testEq(() => L.maximumBy(R.length, L.elems, ['x', 'xx', 'y', 'yy']), 'xx')
  testEq(() => L.minimumBy(R.length, L.elems, ['x', 'xx', 'y', 'yy']), 'x')
  testEq(() => L.maximumBy(I.id, flatten, [[1, 2], [], [2]]), 2)
  testEq(() => L.maximumBy('x', L.elems, [{x: 3}, {x: 1}, {x: 4}, {x: 1}]), {
    x: 4
  })
  testEq(() => L.maximum(L.elems, [1, 2, 3]), 3)
  testEq(() => L.minimumBy(R.negate, L.elems, [1, 2, 3]), 3)
  testEq(() => L.minimumBy('x', L.elems, [{x: 3}, {x: 1}, {x: 4}, {x: 1}]), {
    x: 1
  })
  testEq(() => L.minimum(L.elems, [1, 2, 3]), 1)
  testEq(() => L.mean(L.elems, [3, 4, 2]), 3)
  testEq(() => L.mean([L.elems, 'x'], [{x: 3}, {y: 4}, {x: 2}]), 2.5)
  testEq(() => L.mean(L.elems, []), NaN)
  testEq(() => L.meanAs(x => x + 1, L.elems, []), NaN)
  testEq(() => L.meanAs((x, i) => x + i, L.elems, [1, 1, 1]), 2)
  testEq(() => L.meanAs(x => (x < 0 ? undefined : x), L.elems, [-1, 0, 1]), 0.5)
  testEq(() => L.sum([L.elems, 'x'], undefined), 0)
  testEq(() => L.product([L.elems, 'x'], undefined), 1)
  testEq(
    () =>
      L.sumAs(
        x => (x === undefined ? 0 : R.negate(x)),
        [L.elems, 'x'],
        [{x: -2}, {y: 1}, {x: -3}]
      ),
    5
  )
  testEq(() => L.sum([L.elems, 'x'], [{x: -2}, {y: 1}, {x: -3}]), -5)
  testEq(
    () =>
      L.productAs(
        x => (x === undefined ? 1 : x + 1),
        [L.elems, 'x'],
        [{x: -2}, {y: 1}, {x: -3}]
      ),
    2
  )
  testEq(() => L.product([L.elems, 'x'], [{x: -2}, {y: 1}, {x: -3}]), 6)
  testEq(() => L.join(', ', L.elems, []), '')
  testEq(() => L.join(', ', L.elems, [1, 2, 3]), '1, 2, 3')
  testEq(() => L.join(', ', [L.elems, 'x'], [{x: 1}, {y: 2}, {x: 3}]), '1, 3')
  testEq(() => L.joinAs(x => '(' + x + ')', ', ', L.elems, [1, 2]), '(1), (2)')
  testEq(() => L.foldr((x, y) => [x, y], 0, [L.elems, L.elems], []), 0)
  testEq(() => L.foldl((x, y) => [x, y], 0, [L.elems, L.elems], []), 0)
  testEq(
    () => L.foldr((x, y) => [x, y], 0, [L.elems, L.elems], [[1, 2], [3]]),
    [[[0, 3], 2], 1]
  )
  testEq(
    () => L.foldr((x, y, i) => [x, y, i], 0, [L.elems, L.elems], [[1, 2], [3]]),
    [[[0, 3, 0], 2, 1], 1, 0]
  )
  testEq(
    () => L.foldl((x, y) => [x, y], 0, [L.elems, L.elems], [[1, 2], [3]]),
    [[[0, 1], 2], 3]
  )
  testEq(
    () => L.foldl((x, y, i) => [x, y, i], 0, [L.elems, L.elems], [[1, 2], [3]]),
    [[[0, 1, 0], 2, 1], 3, 0]
  )
  testEq(() => L.countIf((x, i) => i & 1, L.elems, [1, 2, 3]), 1)
  testEq(
    () => L.count([L.elems, L.orElse('x', 'y')], [{x: 11}, {z: 33}, {y: 22}]),
    2
  )
  testEq(() => L.count(flatten, [[], {}, [[[], [{x: [], y: []}], {}]]]), 0)
  testEq(
    () =>
      Array.from(
        L.countsAs(L.get(0), L.elems, [
          ['x', 1],
          ['y', 2],
          ['x', 3],
          [],
          [0, 4],
          ['0', 5],
          ['y', 6]
        ]).entries()
      ),
    [['x', 2], ['y', 2], [undefined, 1], [0, 1], ['0', 1]]
  )
  testEq(
    () =>
      Array.from(
        L.counts(
          [L.elems, 0],
          [['x', 1], ['y', 2], ['x', 3], [], [0, 4], ['0', 5], ['y', 6]]
        ).entries()
      ),
    [['x', 2], ['y', 2], [undefined, 1], [0, 1], ['0', 1]]
  )
  ;[X.foldl, X.foldr].forEach(fold => {
    testEq(() => fold((x, y) => x + y, 0, X.elems, a100000), 100000)
  })
})

describe('L.pick', () => {
  testEq(() => L.get(L.pick({x: 'c'}), {a: [2], b: 1}), undefined)
  testEq(() => L.get(L.pick({x: {y: 'z'}}), null), undefined)
  testEq(() => L.set([L.pick({x: 'c'}), 'x'], 4, {a: [2], b: 1}), {
    a: [2],
    b: 1,
    c: 4
  })
  testEq(() => L.get(L.pick({x: 'b', y: 'a'}), {a: [2], b: 1}), {x: 1, y: [2]})
  testEq(() => L.set([L.pick({x: 'b', y: 'a'}), 'x'], 3, {a: [2], b: 1}), {
    a: [2],
    b: 3
  })
  testEq(() => L.remove([L.pick({x: 'b', y: 'a'}), 'y'], {a: [2], b: 1}), {
    b: 1
  })
  testEq(() => L.remove([L.pick({x: 'b'}), 'x'], {a: [2], b: 1}), {a: [2]})
  testEq(() => L.get(L.pick({x: 0, y: 1}), ['a', 'b']), {x: 'a', y: 'b'})

  testEq(
    () =>
      L.get(L.pick({x: {y: 'a', z: 'b'}, b: ['c', 0]}), {a: 1, b: 2, c: [3]}),
    {x: {y: 1, z: 2}, b: 3}
  )
  testEq(
    () =>
      L.set(
        L.pick({x: {y: 'a', z: 'b'}, b: ['c', 0]}),
        {x: {y: 4}, b: 5, z: 2},
        {a: 1, b: 2, c: [3]}
      ),
    {a: 4, c: [5]}
  )
})

describe('L.pickIn', () => {
  testEq(
    () =>
      L.get(L.pickIn({meta: {file: [], ext: []}}), {
        meta: {file: './foo.txt', base: 'foo', ext: 'txt'}
      }),
    {meta: {file: './foo.txt', ext: 'txt'}}
  )
})

describe('L.props', () => {
  testEq(() => L.get(L.props('x', 'y'), {x: 1, y: 2, z: 3}), {x: 1, y: 2})
  testEq(() => L.get(L.props('x', 'y'), {z: 3}), undefined)
  testEq(() => L.get(L.props('x', 'y'), {x: 2, z: 3}), {x: 2})
  testEq(() => L.remove(L.props('x', 'y'), {x: 1, y: 2, z: 3}), {z: 3})
  testEq(() => L.set(L.props('x', 'y'), {}, {x: 1, y: 2, z: 3}), {z: 3})
  testEq(() => L.set(L.props('x', 'y'), {y: 4}, {x: 1, y: 2, z: 3}), {
    y: 4,
    z: 3
  })
  testEq(() => L.remove(L.props('x', 'y'), {x: 1, y: 2}), {})
  testEq(() => L.set(L.props('a', 'b'), {a: 2}, {a: 1, b: 3}), {a: 2})
  testEq(() => I.keys(L.get(L.props('x', 'b', 'y'), {b: 1, y: 1, x: 1})), [
    'x',
    'b',
    'y'
  ])
})

describe('L.propsExcept', () => {
  testEq(() => L.get(L.propsExcept('x', 'y'), {x: 1, y: 2}), undefined)
  testEq(() => L.get(L.propsExcept('x', 'y'), {x: 1, z: 2}), {z: 2})
  testEq(() => L.set(L.propsExcept('x'), {z: 3}, {x: 1, y: 2}), {x: 1, z: 3})
  testEq(() => L.remove(L.propsExcept('x'), {x: 1, y: 2}), {x: 1})
})

describe('L.assign', () => {
  testEq(() => L.assign([], {x: 2, z: 2}, {x: 1, y: 1, z: 1}), {
    x: 2,
    y: 1,
    z: 2
  })
})

describe('L.getInverse', () => {
  testEq(() => L.getInverse(offBy1, undefined), undefined)
  testEq(() => L.getInverse(offBy1, 1), 0)
})

describe('L.lazy', () => {
  testEq(() => L.collect(flatten, [[[1], 2], 3, [4, [[5]], [6]]]), [
    1,
    2,
    3,
    4,
    5,
    6
  ])
  testEq(() => L.modify(flatten, x => x + 1, [[[1], 2], 3, [4, [[5]], [6]]]), [
    [[2], 3],
    4,
    [5, [[6]], [7]]
  ])
  testEq(
    () =>
      L.modify(flatten, x => (3 <= x && x <= 5 ? undefined : x), [
        [[1], 2],
        3,
        [4, [[5]], [6]]
      ]),
    [[[1], 2], [[[]], [6]]]
  )
})

describe('L.inverse', () => {
  testEq(() => L.get(L.inverse(offBy1), undefined), undefined)
  testEq(() => L.get(L.inverse(offBy1), 1), 0)
  testEq(() => L.getInverse(L.inverse(offBy1), 0), 1)
  testEq(() => L.remove(['x', L.inverse(offBy1)], {x: 1}), {})
})

describe('L.complement', () => {
  testEq(() => L.get(L.complement, undefined), undefined)
  testEq(() => L.set(L.complement, undefined, true), undefined)
  testEq(() => L.get(L.complement, true), false)
  testEq(() => L.set(L.complement, true, undefined), false)
})

describe('L.branch', () => {
  testEq(
    () => L.modify(L.branchOr([], {x: []}), R.identity, {x: 0, y: NaN, z: 0}),
    {
      x: 0,
      y: NaN,
      z: 0
    }
  )
  testEq(() => L.modify(L.branch({}), x => x + 1, null), null)
  testEq(() => L.modify(L.branch({}), x => x + 1, 'anything'), 'anything')
  testEq(() => L.modify(L.branch({}), x => x + 1, {}), {})
  testEq(() => L.set(L.branch({x: []}), 1, 9), {x: 1})
  testEq(() => L.remove(L.branch({x: []}), 1), {})
  testEq(() => L.remove(L.branch({}), {}), {})
  testEq(() => L.modify(L.branch({}), x => x + 1, {x: 1}), {x: 1})
  testEq(
    () =>
      L.modify(L.branch({a: 'x', b: [], c: 0, d: L.identity}), x => x + 1, {
        a: {x: 1},
        b: 2,
        c: [3],
        d: 4,
        extra: 'one'
      }),
    {a: {x: 2}, b: 3, c: [4], d: 5, extra: 'one'}
  )
  testEq(() => L.set(L.branch({a: ['x', 0], b: []}), 0, null), {
    a: {x: [0]},
    b: 0
  })
  testEq(() => L.modify(L.branch({y: L.identity}), R.inc, new XYZ(3, 1, 4)), {
    x: 3,
    y: 2,
    z: 4
  })
  testEq(() => L.or(L.branch({x: [], y: []}), {x: false, y: false}), false)

  testEq(
    () => L.modify(L.branch({x: {a: []}, y: []}), R.negate, {x: {a: 1}, y: 2}),
    {x: {a: -1}, y: -2}
  )
})

describe('L.branchOr', () => {
  testEq(
    () =>
      L.transform(L.branchOr(L.modifyOp(R.inc), {x: L.modifyOp(R.dec)}), {
        x: 1,
        y: 1
      }),
    {x: 0, y: 2}
  )
})

describe('L.branches', () => {
  testEq(() => L.collect(L.branches('a', 'b'), {a: 2, b: 3}), [2, 3])
})

describe('L.removable', () => {
  testEq(() => L.set(L.removable('x'), 42, 'non object'), 42)
  testEq(() => L.get(L.removable('x'), {x: 1, y: 2}), {x: 1, y: 2})
  testEq(() => L.get([L.removable('y'), 'y'], {x: 1, y: 2}), 2)
  testEq(() => L.set([L.removable('y'), 'y'], 3, {x: 1, y: 2}), {x: 1, y: 3})
  testEq(
    () => L.set([L.removable('x'), 'x'], undefined, {x: 1, y: 2}),
    undefined
  )
})

describe('L.is', () => {
  testEq(() => L.get(L.is('foo'), 'bar'), false)
  testEq(() => L.get(L.is('foo'), undefined), false)
  testEq(() => L.get(L.is('foo'), 'foo'), true)
  testEq(() => L.set(L.is('foo'), false, 'bar'), undefined)
  testEq(() => L.set(L.is('foo'), undefined, 'bar'), undefined)
  testEq(() => L.set(L.is('foo'), 'bar', 'bar'), undefined)
  testEq(() => L.set(L.is('foo'), true, 'bar'), 'foo')
  testEq(() => L.set(L.is('foo'), true, undefined), 'foo')
})

describe('indexing', () => {
  testEq(() => L.modify(L.identity, (x, i) => [typeof x, typeof i], 0), [
    'number',
    'undefined'
  ])
  testEq(() => L.modify(['x', 0], (x, i) => [x, i], {x: ['y']}), {
    x: [['y', 0]]
  })
  testEq(() => L.modify(['x', L.required([])], (x, i) => [x, i], {x: ['y']}), {
    x: [['y'], 'x']
  })
  testEq(() => L.modify(L.elems, (x, i) => (i & 1 ? -x : x), [1, 2, 3, 4]), [
    1,
    -2,
    3,
    -4
  ])
  testEq(
    () => L.modify([L.elems, L.when((_, i) => i & 1)], x => -x, [1, 2, 3, 4]),
    [1, -2, 3, -4]
  )
  testEq(() => L.collectAs((x, i) => [x, i], L.elems, ['a', 'b']), [
    ['a', 0],
    ['b', 1]
  ])
  testEq(() => L.collectAs((x, i) => [x, i], L.values, {x: 101, y: 42}), [
    [101, 'x'],
    [42, 'y']
  ])
})

describe('L.toFunction', () => {
  testEq(() => typeof L.toFunction(1), 'function')
  testEq(() => typeof L.toFunction('x'), 'function')
  testEq(() => typeof L.toFunction(L.find(I.id)), 'function')
})

describe('L.ungroupBy', () => {
  testEq(() => L.get(L.ungroupBy(I.id), [[]]), undefined)
  testEq(() => L.get(L.ungroupBy('x'), [[{y: 1}]]), undefined)
  testEq(() => L.get(L.ungroupBy(L.get('x')), [[{x: 1}, {x: 2}]]), undefined)
  testEq(() => L.getInverse(L.ungroupBy('x'), [{y: 1}]), undefined)
})

describe('L.groupBy', () => {
  testEq(() => L.getInverse(L.groupBy(I.id), [[]]), undefined)
  testEq(() => L.getInverse(L.groupBy('x'), [[{y: 1}]]), undefined)
  testEq(
    () => L.getInverse(L.groupBy(L.get('x')), [[{x: 1}, {x: 2}]]),
    undefined
  )
  testEq(() => L.get(L.groupBy('x'), [{y: 1}]), undefined)
})

describe('L.zipWith1', () => {
  testEq(() => L.get(L.zipWith1(L.identity), [42, []]), undefined)
  testEq(() => L.getInverse(L.zipWith1(L.identity), ['not a pair']), undefined)
  testEq(
    () =>
      L.getInverse(L.zipWith1(L.identity), [['not', 'a'], ['equal', 'value']]),
    undefined
  )
})

describe('L.unzipWith1', () => {
  testEq(() => L.getInverse(L.unzipWith1(L.identity), [42, []]), undefined)
  testEq(() => L.get(L.unzipWith1(L.identity), ['not a pair']), undefined)
  testEq(
    () => L.get(L.unzipWith1(L.identity), [['not', 'a'], ['equal', 'value']]),
    undefined
  )
})

const flatteningRules = L.alternatives(
  L.mapping(o => [{...o, children: []}, [{...o, ps: []}]]),
  [
    L.mapping((cs, o) => [{...o, children: cs}, [o, cs]]),
    L.zipWith1(
      L.mapping((p, c, ps) => [
        [p, {...c, parents: ps}],
        {...c, ps: [p, ...ps]}
      ])
    )
  ],
  [
    L.ungroupBy(['ps', L.choices([0, 'id'], [])]),
    L.arrays(L.mapping((o, ps) => [{...o, ps}, {...o, parents: ps}]))
  ]
)

describe('L.attemptSomeDown', () => {
  testEq(
    () =>
      L.get(L.attemptSomeDown(flatteningRules), [
        {
          id: 1,
          children: [
            {id: 2, children: [{id: 3, children: []}]},
            {id: 4, children: []}
          ]
        },
        {
          id: 5,
          children: [
            {id: 6, children: []},
            {id: 7, children: [{id: 8, children: []}]},
            {id: 9, children: []}
          ]
        }
      ]),
    [
      {
        id: 1,
        children: [{id: 2, children: [[{ps: [], id: 3}]]}, [{ps: [], id: 4}]]
      },
      {
        id: 5,
        children: [
          [{ps: [], id: 6}],
          {id: 7, children: [[{ps: [], id: 8}]]},
          [{ps: [], id: 9}]
        ]
      }
    ]
  )
})

describe('L.attemptEveryUp', () => {
  testEq(() => L.get(L.attemptEveryUp([]), undefined), undefined)
  testEq(
    () =>
      L.get(L.attemptEveryUp(flatteningRules), [
        {
          id: 1,
          children: [
            {id: 2, children: [{id: 3, children: []}]},
            {id: 4, children: []}
          ]
        },
        {
          id: 5,
          children: [
            {id: 6, children: []},
            {id: 7, children: [{id: 8, children: []}]},
            {id: 9, children: []}
          ]
        }
      ]),
    [
      {parents: [{id: 1}, {id: 2}], id: 3},
      {parents: [{id: 1}], id: 4},
      {parents: [{id: 5}], id: 6},
      {parents: [{id: 5}, {id: 7}], id: 8},
      {parents: [{id: 5}], id: 9}
    ]
  )
  testEq(
    () =>
      L.set([L.attemptEveryUp(flatteningRules), L.elems, 'type'], 'leaf', [
        {
          id: 1,
          children: [
            {id: 2, children: [{id: 3, children: []}]},
            {id: 4, children: []}
          ]
        },
        {
          id: 5,
          children: [
            {id: 6, children: []},
            {id: 7, children: [{id: 8, children: []}]},
            {id: 9, children: []}
          ]
        }
      ]),
    [
      {
        children: [
          {children: [{children: [], id: 3, type: 'leaf'}], id: 2},
          {children: [], id: 4, type: 'leaf'}
        ],
        id: 1
      },
      {
        children: [
          {children: [], id: 6, type: 'leaf'},
          {children: [{children: [], id: 8, type: 'leaf'}], id: 7},
          {children: [], id: 9, type: 'leaf'}
        ],
        id: 5
      }
    ]
  )
})

describe('L.attemptEveryDown', () => {
  testEq(
    () =>
      L.get(L.attemptEveryDown(L.inverse(flatteningRules)), [
        {parents: [{id: 1}, {id: 2}], id: 3},
        {parents: [{id: 1}], id: 4},
        {parents: [{id: 5}], id: 6},
        {parents: [{id: 5}, {id: 7}], id: 8},
        {parents: [{id: 5}], id: 9}
      ]),
    [
      {
        id: 1,
        children: [
          {id: 2, children: [{id: 3, children: []}]},
          {id: 4, children: []}
        ]
      },
      {
        id: 5,
        children: [
          {id: 6, children: []},
          {id: 7, children: [{id: 8, children: []}]},
          {id: 9, children: []}
        ]
      }
    ]
  )
})

describe('BST', () => {
  const randomInt = (min, max) => Math.floor(Math.random() * (max - min)) + min
  const randomPick = (...choices) => choices[randomInt(0, choices.length)]

  it('maintains validity through operations', () => {
    let before
    let after
    let op
    let key

    const error = () => {
      throw Error(
        'From ' +
          show(before) +
          ' ' +
          op +
          ' with ' +
          key +
          ' gave ' +
          show(after)
      )
    }

    for (let i = 0; i < 1000; ++i) {
      key = randomInt(0, 10)
      op = randomPick('set', 'delete')

      switch (op) {
        case 'set':
          after = L.set(BST.valueOf(key), key, before)
          if (undefined === L.get(BST.valueOf(key), after)) error()
          break
        case 'delete':
          after = L.remove(BST.valueOf(key), before)
          if (undefined !== L.get(BST.valueOf(key), after)) error()
          break
      }

      if (!BST.isValid(after)) error()

      before = after
    }
  })

  testEq(
    () =>
      I.seq(
        [['m', 1], ['a', 2], ['g', 3], ['i', 4], ['c', 5]],
        BST.fromPairs,
        L.modify(BST.values, x => -x)
      ),
    I.seq(
      [['m', -1], ['a', -2], ['g', -3], ['i', -4], ['c', -5]],
      BST.fromPairs
    )
  )
})

describe('L.seq', () => {
  testEq(() => L.set(L.seq(), 'ignored', 'anything'), 'anything')
  testEq(() => L.set([L.seq(), 'x'], 'ignored', {x: 'anything'}), {
    x: 'anything'
  })
  testEq(() => L.set(L.seq('x', 'y', 'z'), 1, undefined), {x: 1, y: 1, z: 1})
  testEq(() => L.modify(everywhere, x => [x], {x: {y: 1}}), [{x: [{y: [1]}]}])

  testEq(() => collectM(L.seq(1, 0, 2), ['b', 'a', 'c']), ['a', 'b', 'c'])
})

describe('lazy folds', () => {
  testEq(() => L.get([L.elems, 'y'], [{x: 1}, {y: 2}, {z: 3}]), 2)
  testEq(() => L.get(flatten, [[[[[[[[[[101]]]]]]]]]]), 101)
  testEq(() => L.get(L.elems, []), undefined)
  testEq(() => L.get(L.values, {}), undefined)
  testEq(
    () =>
      L.getAs((x, i) => (x > 3 ? [x + 2, i] : undefined), L.elems, [
        3,
        1,
        4,
        1,
        5
      ]),
    [6, 2]
  )
  testEq(
    () =>
      L.getAs((x, i) => (x > 3 ? [x + 2, i] : undefined), L.values, {
        a: 3,
        b: 1,
        c: 4,
        d: 1,
        e: 5
      }),
    [6, 'c']
  )
  testEq(() => L.getAs(_ => {}, L.values, {x: 1}), undefined)
  testEq(
    () =>
      L.getAs(x => (x < 9 ? undefined : [x]), flatten, [
        [[1], 2],
        {y: 3},
        [{l: 41, r: [5]}, {x: 6}]
      ]),
    [41]
  )
  testEq(
    () => {
      let n = 0
      const v = X.getAs(
        x => {
          n += 1
          return x === 42 ? x : undefined
        },
        X.elems,
        [1, 3, 42, 56, 32]
      )
      return [n, v]
    },
    [3, 42]
  )
  testEq(
    () => {
      let n = 0
      const v = X.getAs(
        x => {
          n += 1
          return x === 42 ? x : undefined
        },
        X.values,
        {x: 1, y: 42, z: 25}
      )
      return [n, v]
    },
    [2, 42]
  )
  testEq(
    () => {
      let n = 0
      const v = X.getAs(
        x => {
          n += 1
          return x === 42 ? x : undefined
        },
        X.branch({x: [], y: 0, z: []}),
        {x: 5, z: 5, y: [42]}
      )
      return [n, v]
    },
    [2, 42]
  )
  testEq(
    () => {
      let n = 0
      const v = X.getAs(
        x => {
          n += 1
          return x === 'ba' ? x : undefined
        },
        X.matches(/[ab]+/g),
        'Ab-ba CD b'
      )
      return [n, v]
    },
    [2, 'ba']
  )
  testEq(() => L.any((x, i) => x > i, L.elems, [0, 1, 3]), true)
  testEq(() => L.any((x, i) => x > i, L.elems, [0, 1, 2]), false)
  testEq(() => L.all((x, i) => x > i, L.elems, [1, 2, 3]), true)
  testEq(() => L.all((x, i) => x > i, L.elems, [1, 2, 2]), false)
  testEq(() => L.all1((x, i) => x > i, L.elems, [1, 2, 3]), true)
  testEq(() => L.all1((x, i) => x > i, L.elems, []), false)
  testEq(() => L.none((x, i) => x > i, L.elems, [0, 1, 3]), false)
  testEq(() => L.none((x, i) => x > i, L.elems, [0, 1, 2]), true)
  testEq(() => L.and(L.elems, []), true)
  testEq(() => L.and1(L.elems, [1]), true)
  testEq(() => L.and1(L.elems, [1, 0]), false)
  testEq(() => L.and1(L.elems, []), false)
  testEq(() => L.or(L.elems, []), false)
})

describe('L.first', () => {
  testEq(() => L.get(L.first, undefined), undefined)
  testEq(() => L.get(L.first, []), undefined)
  testEq(() => L.get(L.first, [5]), 5)
  testEq(() => L.set(L.first, 5, undefined), [5])
  testEq(() => L.set(L.first, 5, []), [5])
  testEq(() => L.set(L.first, 5, [1, 2]), [5, 2])
  testEq(() => L.get([L.first, (_, i) => i], ['a', 'b']), 0)
})

describe('L.last', () => {
  testEq(() => L.get(L.last, undefined), undefined)
  testEq(() => L.get(L.last, []), undefined)
  testEq(() => L.get(L.last, [5]), 5)
  testEq(() => L.set(L.last, 5, undefined), [5])
  testEq(() => L.set(L.last, 5, []), [5])
  testEq(() => L.set(L.last, 5, [1, 2]), [1, 5])
  testEq(() => L.get([L.last, (_, i) => i], ['a', 'b']), 1)
})

describe('standard isos', () => {
  testEq(
    () => L.getInverse(L.uri, 'http://www.Not a URL.com'),
    'http://www.Not%20a%20URL.com'
  )
  testEq(
    () => L.get(L.uri, 'http://www.Not%20a%20URL.com'),
    'http://www.Not a URL.com'
  )
  testEq(() => L.getInverse(L.uri, null), undefined)
  testEq(() => L.get(L.uri, '%') instanceof Error, true)

  testEq(
    () => L.getInverse(L.uriComponent, 'Hello, world!'),
    'Hello%2C%20world!'
  )
  testEq(() => L.get(L.uriComponent, 'Hello%2C%20world!'), 'Hello, world!')
  testEq(() => L.getInverse(L.uriComponent, {}), undefined)
  testEq(() => L.getInverse(L.uriComponent, 101), '101')
  testEq(() => L.getInverse(L.uriComponent, true), 'true')
  testEq(() => L.get(L.uriComponent, '%') instanceof Error, true)

  testEq(
    () => L.getInverse(L.json({space: 2}), {this: ['Is', true]}),
    '{\n  "this": [\n    "Is",\n    true\n  ]\n}'
  )
  testEq(() => L.get(L.json(undefined), '{"this":["Is",true]}'), {
    this: ['Is', true]
  })
  testEq(() => L.getInverse(L.json(), undefined), undefined)
  testEq(() => L.get(L.json(), '%') instanceof Error, true)
})

describe('L.matches', () => {
  testEq(() => L.collect(L.matches(/\w+/g), 'Hello, world!'), [
    'Hello',
    'world'
  ])
  testEq(() => L.and(L.matches(/\w+/g), 'This is another test!'), true)
  testEq(
    () => L.modify(L.matches(/\w+/g), R.toUpper, 'Hello, world!'),
    'HELLO, WORLD!'
  )
  testEq(
    () =>
      L.modify(L.matches(/does not match/g), R.toUpper, "what does't match"),
    "what does't match"
  )
  testEq(
    () => L.modify(L.matches(/does not matter/g), R.toUpper, ['Not a string']),
    ['Not a string']
  )
  testEq(() => L.or(L.matches(/does not matter/g), ['Not a string']), false)
  testEq(() => L.set(L.matches(/\w+|\W+/g), '', 'Hello, world!'), '')
  testEq(() => L.remove(L.matches(/\w+|\W+/g), 'Hello, world!'), '')

  testEq(() => L.collect(L.matches(/a?b?/g), 'x'), [])

  testEq(() => L.get(L.matches(/\w+/), 'Hello, world!'), 'Hello')
  testEq(
    () => L.set(L.matches(/\w+/), 'Salut', 'Hello, world!'),
    'Salut, world!'
  )
  testEq(() => L.get(L.matches(/does not match/), 'Hello, world!'), undefined)
  testEq(
    () => L.set(L.matches(/does not match/), 'Anything', 'Hello, world!'),
    'Hello, world!'
  )
  testEq(
    () => L.set(L.matches(/does not match/), 'Anything', {not_a_string: true}),
    {not_a_string: true}
  )
  testEq(() => L.set(L.matches(/\w+/g), '', 'Hello'), '')
  testEq(() => L.remove(L.matches(/\w+/g), 'Hello'), '')
  testEq(() => L.remove(L.matches(/^/), ''), '')
})

describe('L.foldTraversalLens', () => {
  testEq(() => L.get(L.foldTraversalLens(L.maximum, L.elems), [3, 1, 4, 1]), 4)
  testEq(
    () => L.set(L.foldTraversalLens(L.maximum, L.elems), 2, [3, 1, 4, 1]),
    [2, 2, 2, 2]
  )
})

describe('L.collectTotal', () => {
  testEq(() => L.collectTotal([L.elems, 'x'], [{x: 'a'}, {y: 'b'}]), [
    'a',
    undefined
  ])
})

describe('L.collectTotalAs', () => {
  testEq(
    () =>
      L.collectTotalAs(
        (v, i) => (v ? undefined : i),
        [L.elems, 'x'],
        [{x: 'a'}, {y: 'b'}]
      ),
    [undefined, 'x']
  )
})

describe('L.disperse', () => {
  testEq(
    () => L.disperse(['xs', L.elems], {not: 'an array-like'}, {xs: [3, 1, 4]}),
    {xs: []}
  )
  testEq(
    () =>
      L.disperse([L.elems, 'x'], [-3, undefined, -4], [{x: 3}, {x: 1}, {x: 4}]),
    [{x: -3}, {}, {x: -4}]
  )
  testEq(
    () =>
      L.disperse(
        [L.elems, L.assignTo],
        [{x: 1}, {z: 0}, {y: 2}],
        [{y: 3}, null, {x: 1}]
      ),
    [{y: 3, x: 1}, {z: 0}, {x: 1, y: 2}]
  )
})

describe('L.partsOf', () => {
  testEq(() => L.getInverse(L.partsOf(L.branches('x', 'y')), [4, 2]), {
    x: 4,
    y: 2
  })
  testEq(
    () =>
      L.get(L.partsOf(L.values, 1), {
        foo: [false, 'oldUser1'],
        bar: [true, 'oldUser2'],
        quux: [false, 'oldUser3']
      }),
    ['oldUser1', 'oldUser2', 'oldUser3']
  )
  testEq(
    () =>
      L.set(L.partsOf(L.values, 1), ['user1', 'user2', 'user3'], {
        foo: [false, 'oldUser1'],
        bar: [true, 'oldUser2'],
        quux: [false, 'oldUser3']
      }),
    {foo: [false, 'user1'], bar: [true, 'user2'], quux: [false, 'user3']}
  )
  testEq(
    () =>
      L.remove(L.partsOf(L.values, 1), {
        foo: [false, 'oldUser1'],
        bar: [true, 'oldUser2'],
        quux: [false, 'oldUser3']
      }),
    {foo: [false], bar: [true], quux: [false]}
  )
  testEq(
    () =>
      L.get(
        L.pick({
          results: L.partsOf(L.flat('results')),
          total: [L.elems, 'total']
        }),
        [
          {total: 3, and: 'also1', results: ['a']},
          {total: 3, and: 'also2', results: ['b']},
          {total: 3, and: 'also3', results: ['c']}
        ]
      ),
    {results: ['a', 'b', 'c'], total: 3}
  )
  testEq(
    () =>
      L.set(
        L.pick({
          results: L.partsOf(L.flat('results')),
          total: [L.elems, 'total']
        }),
        {total: 4, results: ['C', 'B', 'A']},
        [
          {total: 3, and: 'also1', results: ['a']},
          {total: 3, and: 'also2', results: ['b']},
          {total: 3, and: 'also3', results: ['c']}
        ]
      ),
    [
      {total: 4, and: 'also1', results: ['C']},
      {total: 4, and: 'also2', results: ['B']},
      {total: 4, and: 'also3', results: ['A']}
    ]
  )
  testEq(
    () =>
      L.remove(
        L.pick({
          results: L.partsOf(L.flat('results')),
          total: [L.elems, 'total']
        }),
        [
          {total: 3, and: 'also1', results: ['a']},
          {total: 3, and: 'also2', results: ['b']},
          {total: 3, and: 'also3', results: ['c']}
        ]
      ),
    [
      {and: 'also1', results: []},
      {and: 'also2', results: []},
      {and: 'also3', results: []}
    ]
  )
})

describe('transforming', () => {
  testEq(() => L.transform(L.assignOp({y: 2}), {x: 1, y: 1, z: 1}), {
    x: 1,
    y: 2,
    z: 1
  })
  testEq(() => L.transform([L.elems, L.modifyOp(x => x + 1)], [1, 2, 3]), [
    2,
    3,
    4
  ])
  testEq(() => L.transform([L.elems, L.setOp(4)], [1, 2, 3]), [4, 4, 4])
  testEq(
    () =>
      L.transform([L.elems, L.when(x => x > 3), L.removeOp], [3, 1, 4, 1, 5]),
    [3, 1, 1]
  )
  testEq(() => L.get(L.setOp(42), 101), undefined)
  testEq(() => L.set(L.setOp(42), 96, 101), 42)
})

describe('L.reIx', () => {
  testEq(() => L.modify(L.reIx(L.flatten), R.pair, [[3], [[[1]], 4], [1], 5]), [
    [[3, 0]],
    [[[[1, 1]]], [4, 2]],
    [[1, 3]],
    [5, 4]
  ])
})

describe('L.offset', () => {
  testEq(
    () =>
      L.collect(L.offset(2, [L.flatten, L.when(R.lt(1))]), [
        [3],
        [[[1]], 4],
        [1],
        5
      ]),
    [5]
  )
  testEq(
    () =>
      L.modify(L.offset(2, [L.flatten, L.when(R.lt(1))]), R.negate, [
        [3],
        [[[1]], 4],
        [1],
        5
      ]),
    [[3], [[[1]], 4], [1], -5]
  )
})

describe('L.subseq', () => {
  testEq(
    () =>
      L.collect(L.subseq(1, 2, [L.flatten, L.when(R.lt(1))]), [
        [3],
        [[[1]], 4],
        [1],
        5
      ]),
    [4]
  )
})

describe('L.cond', () => {
  testEq(() => L.set(L.cond([R.not, L.setOp(1)]), 3, 2), 2)
  testEq(() => L.set(L.cond([R.not, L.setOp(1)]), 3, 0), 1)
  testEq(() => L.transform(L.cond([R.not, L.setOp(1)], [L.setOp(0)]), null), 1)
  testEq(() => L.transform(L.cond([R.not, L.setOp(1)], [L.setOp(0)]), 2), 0)
  testEq(
    () =>
      L.transform(
        L.cond(
          [R.equals(1), L.setOp(-1)],
          [R.equals(2), L.setOp(1)],
          [L.setOp(2)]
        ),
        -1
      ),
    2
  )
  testEq(
    () =>
      L.transform(
        L.cond(
          [R.equals(1), L.setOp(-1)],
          [R.equals(2), L.setOp(1)],
          [R.T, L.setOp(2)]
        ),
        -1
      ),
    2
  )
  testEq(
    () =>
      L.transform(
        L.cond(
          [R.equals(1), L.setOp(-1)],
          [R.equals(2), L.setOp(1)],
          [L.setOp(2)]
        ),
        2
      ),
    1
  )
})

describe('L.condOf', () => {
  testEq(
    () =>
      L.collect(
        [
          L.elems,
          L.condOf(
            'type',
            [R.equals('a'), 'foo'],
            [R.equals('b'), 'bar'],
            ['lol']
          )
        ],
        [{type: 'a', foo: 42}, {type: 'b', bar: 101}, {type: 'c', lol: 76}]
      ),
    [42, 101, 76]
  )
  testEq(
    () =>
      L.collect(
        [
          L.elems,
          L.condOf('type', [R.equals('a'), 'foo'], [R.equals('b'), 'bar'])
        ],
        [{type: 'a', foo: 42}, {type: 'b', bar: 101}, {type: 'c', lol: 76}]
      ),
    [42, 101]
  )
  testEq(() => L.get(L.condOf([]), 'anything'), undefined)
  testEq(
    () =>
      L.get(
        L.condOf(
          ['c', L.elems],
          [R.equals(1), ['d', 0]],
          [R.equals(2), ['d', 1]],
          [R.equals(3), ['d', 2]]
        ),
        {
          c: [3, 1, 2],
          d: ['a', 'b', 'c']
        }
      ),
    'a'
  )
  testEq(
    () =>
      L.get(
        L.condOf(
          ['c', L.elems],
          [R.equals(1), ['d', 0]],
          [R.equals(2), ['d', 1]],
          [R.equals(3), ['d', 2]]
        ),
        {
          c: [3, -1, 2],
          d: ['a', 'b', 'c']
        }
      ),
    'b'
  )
})

describe('L.ifElse', () => {
  testEq(() => L.set(L.ifElse(R.not, L.setOp(1), L.zero), 3, 2), 2)
  testEq(() => L.set(L.ifElse(R.not, L.setOp(1), L.zero), 3, 0), 1)
  testEq(() => L.transform(L.ifElse(R.not, L.setOp(1), L.setOp(0)), null), 1)
  testEq(() => L.transform(L.ifElse(R.not, L.setOp(1), L.setOp(0)), 2), 0)
})

describe('L.singleton', () => {
  testEq(() => L.get(L.singleton, ['too', 'long']), undefined)
  testEq(() => L.get(L.singleton, 'too-long'), undefined)
  testEq(() => L.get(L.singleton, 'x'), 'x')
  testEq(() => L.get(L.singleton, [101]), 101)
  testEq(() => L.get(L.singleton, {}), undefined)
  testEq(() => L.getInverse(L.singleton)(43), [43])
  testEq(() => L.getInverse(L.singleton, undefined), undefined)
})

describe('L.flatten', () => {
  testEq(() => L.collect(L.flatten, 101), [101])
  testEq(() => L.collect(L.flatten, [['x'], [1, [], {y: 2}], [[false]]]), [
    'x',
    1,
    {y: 2},
    false
  ])
  testEq(() => L.set(L.flatten, 1, undefined), undefined)
  testEq(() => L.set(L.flatten, 1, 'defined'), 1)
})

describe('L.leafs', () => {
  testEq(() => L.collect(L.leafs, 101), [101])
  testEq(() => L.collect(L.leafs, new XYZ(1, 2, 3)), [new XYZ(1, 2, 3)])
  testEq(() => L.collect(L.leafs, [['x'], [1, [], {y: 2}], [[false]]]), [
    'x',
    1,
    2,
    false
  ])
  testEq(() => L.set(L.leafs, 1, undefined), undefined)
  testEq(() => L.set(L.leafs, 1, 'defined'), 1)
})

describe('L.query', () => {
  testEq(
    () =>
      L.modify(L.query(L.choice('a', 'b')), R.inc, [
        {foo: [{a: 1}, {b: 2}]},
        {bar: {b: 3}},
        {a: 4}
      ]),
    [{foo: [{a: 2}, {b: 3}]}, {bar: {b: 4}}, {a: 5}]
  )
  testEq(
    () => L.get(L.query((x, i) => (i === 1 ? x : undefined)), ['a', 'b']),
    'b'
  )
})

describe('L.satisfying', () => {
  testEq(() => L.collect(L.satisfying(R.is(Number)), [3, '1', 4, {x: 1}]), [
    3,
    4,
    1
  ])
})

describe('L.whereEq', () => {
  testEq(
    () =>
      L.collect(L.whereEq({type: 'foo'}), [
        {type: 'foo', children: [{type: 'foo'}]},
        {type: 'bar', children: [{type: 'foo', value: 'bar'}]}
      ]),
    [{type: 'foo', children: [{type: 'foo'}]}, {type: 'foo', value: 'bar'}]
  )
})

describe('L.array', () => {
  testEq(
    () =>
      L.get(L.array(L.pick({x: 'y', z: 'x'})), [{x: 1, y: 2}, {x: 3, y: 4}]),
    [{x: 2, z: 1}, {x: 4, z: 3}]
  )
  testEq(
    () =>
      L.getInverse(L.array(L.pick({x: 'y', z: 'x'})), [
        {x: 2, z: 1},
        {x: 4, z: 3}
      ]),
    [{x: 1, y: 2}, {x: 3, y: 4}]
  )
  testEq(() => L.get(L.array(L.pick({x: 'y', z: 'x'})), []), [])
  testEq(() => L.get(L.array(L.pick({x: 'y', z: 'x'})), {}), undefined)
  testEq(() => L.set(L.array(L.pick({x: 'y', z: 'x'})), [], [{x: 1, y: 2}]), [])
  testEq(() => L.remove([L.array(L.iso(R.toUpper, R.toLower)), 0], ['it']), [])
  testEq(
    () => L.remove([L.array(L.iso(R.toUpper, R.toLower))], ['it']),
    undefined
  )
  testEq(() => L.get(L.array(L.iso(R.toUpper, R.toLower)), 'string'), [
    'S',
    'T',
    'R',
    'I',
    'N',
    'G'
  ])
})

describe('L.arrays', () => {
  testEq(
    () =>
      L.get(L.arrays(L.pick({x: 'y', z: 'x'})), [{x: 1, y: 2}, {x: 3, y: 4}]),
    [{x: 2, z: 1}, {x: 4, z: 3}]
  )
  testEq(
    () =>
      L.getInverse(L.arrays(L.pick({x: 'y', z: 'x'})), [
        {x: 2, z: 1},
        {x: 4, z: 3}
      ]),
    [{x: 1, y: 2}, {x: 3, y: 4}]
  )
  testEq(() => L.get(L.arrays(L.pick({x: 'y', z: 'x'})), []), [])
  testEq(() => L.get(L.arrays(L.pick({x: 'y', z: 'x'})), {}), undefined)
  testEq(
    () => L.set(L.arrays(L.pick({x: 'y', z: 'x'})), [], [{x: 1, y: 2}]),
    []
  )
  testEq(() => L.get(L.arrays(L.split('')), [{not: 'string'}]), undefined)
  testEq(
    () => L.remove([L.arrays(L.iso(R.toUpper, R.toLower))], ['it']),
    undefined
  )
  testEq(
    () => L.get(L.arrays(L.iso(R.toUpper, R.toLower)), 'string'),
    undefined
  )
})

describe('L.cross', () => {
  testEq(() => L.get(L.cross([]), []), [])
  testEq(() => L.get(L.cross([]), {arrayLike: false}), undefined)
  testEq(() => L.get(L.cross([L.negate, L.add(2)]), [1, 2]), [-1, 4])
  testEq(() => L.getInverse(L.cross([L.negate, L.add(2)]), [1, 2]), [-1, 0])

  testEq(() => L.get(L.cross([[], []]), [1]), undefined)
  testEq(() => L.get(L.cross([[], []]), [1, 2, 3]), undefined)
  testEq(() => L.getInverse(L.cross([[], []]), [1]), undefined)
  testEq(() => L.getInverse(L.cross([[], []]), [1, 2, 3]), undefined)

  testEq(() => L.set(L.cross([L.appendTo, 'x']), [1, 2], [[0], {y: 1}]), [
    [0, 1],
    {y: 1, x: 2}
  ])
  testEq(() => L.get(L.cross([0, 'y']), [[101], {y: 1}]), [101, 1])
})

describe('L.subset', () => {
  testEq(() => L.get(L.array(L.subset(R.lt(0))), [1, -2, 3, -4]), [1, 3])
  testEq(
    () =>
      L.collect([L.branches('foo', 'bar'), L.subset(R.has('x'))], {
        foo: {x: 1},
        baz: {x: 2}
      }),
    [{x: 1}]
  )
})

describe('L.unfold', () => {
  testEq(
    () => L.get(L.unfold(L.subset(I.isArray)), [[[null, 'c'], 'b'], 'a']),
    [null, ['a', 'b', 'c']]
  )
  testEq(
    () => L.getInverse(L.unfold(L.subset(I.isArray)), [null, ['a', 'b', 'c']]),
    [[[null, 'c'], 'b'], 'a']
  )
  testEq(() => L.getInverse(L.unfold([]), 'not a pair'), undefined)
  testEq(() => L.getInverse(L.unfold([]), ['not an array', 0]), undefined)
})

describe('L.fold', () => {
  testEq(
    () =>
      L.get(L.fold(L.mapping((t, h) => [[t, h], [h, t]])), [null, [1, 2, 3]]),
    [1, [2, [3, null]]]
  )
})

describe('L.iterate', () => {
  const step = abIa => [
    L.mapping((a, b, bs) => [[a, [b, ...bs]], [[a, b], bs]]),
    L.cross([abIa, L.identity])
  ]
  const foldl = abIa => [L.iterate(step(abIa)), L.mapping(a => [[a, []], a])]
  testEq(
    () =>
      L.get(foldl(L.mapping((xs, x) => [[xs, x], [x, ...xs]])), [
        [],
        [0, 1, 2, 3]
      ]),
    [3, 2, 1, 0]
  )
})

describe('L.pattern', () => {
  testEq(() => L.get(L.pattern((x, y) => ({x, y})), {x: 1, z: 2}), undefined)
})

describe('L.patterns', () => {
  testEq(() => L.get(L.patterns((x, y) => [{x, y}]), {x: 1, y: 2}), {
    x: 1,
    y: 2
  })
  testEq(() => L.get(L.patterns((x, y) => [{x, y}]), {x: 1, z: 2}), undefined)
  testEq(() => L.get(L.patterns(['foo', 'bar']), 'bar'), 'bar')
})

describe('L.mapping', () => {
  testEq(
    () =>
      L.get(L.mapping((x, y) => [[x, L._, ...L._, y], [y, ...L._, x]]), [
        1,
        2,
        3
      ]),
    [3, 1]
  )
  testEq(() => L.get(L.mapping(xs => [xs, [[...xs], [...xs]]]), [1, 2]), [
    [1, 2],
    [1, 2]
  ])
  testEq(() => L.get(L.mapping(xs => [{xs}, {...xs}]), {xs: {foo: 'bar'}}), {
    foo: 'bar'
  })
  testEq(
    () => L.get(L.mapping((x, rest) => [{x, ...rest}, {y: x}]), {x: 1, z: 2}),
    {y: 1}
  )
  testEq(() => L.get(L.mapping((x, rest) => [{x}, {x, ...rest}]), {x: 1}), {
    x: 1
  })
  testEq(() => L.get(L.mapping((f, m, l) => [[f, l], [l, ...m, f]]), [1, 2]), [
    2,
    1
  ])
  testEq(() => L.get(L.mapping(xs => [[...xs, 1], xs]), [1, 2]), undefined)
  testEq(() => L.get(L.mapping(x => [{x, y: [1]}, x]), {x: 1, y: 2}), undefined)
  testEq(
    () => L.get(L.mapping(x => [{x, y: [{x: 1}]}, x]), {x: 42, y: [{x: 1}]}),
    42
  )
  testEq(() => L.get(L.mapping(x => [{x}, x]), {x: 1, y: 2}), undefined)
  testEq(() => L.get(L.mapping(x => [[x, x], x]), [1, 2]), undefined)
  testEq(() => L.get(L.mapping(x => [[x, x], x]), [1, 1]), 1)
  testEq(
    () =>
      L.get(L.mapping((p, ps) => [{p, ...ps}, [...p, ps]]), {
        p: [101, 69],
        more: 42
      }),
    [101, 69, {more: 42}]
  )
  testEq(
    () =>
      L.getInverse(L.mapping((p, ps) => [{p, ...ps}, [...p, ps]]), [
        101,
        69,
        {more: 42}
      ]),
    {p: [101, 69], more: 42}
  )
  testEq(() => L.get(Lambda.ppp, 'λx → (x x)'), {
    type: 'lam',
    param: 'x',
    body: {
      type: 'app',
      fun: {type: 'ref', name: 'x'},
      arg: {type: 'ref', name: 'x'}
    }
  })
  testEq(() => L.get(Lambda.ppp, 'λf → λx → λy → f x y'), {
    type: 'lam',
    param: 'f',
    body: {
      type: 'lam',
      param: 'x',
      body: {
        type: 'lam',
        param: 'y',
        body: {
          type: 'app',
          fun: {
            type: 'app',
            fun: {type: 'ref', name: 'f'},
            arg: {type: 'ref', name: 'x'}
          },
          arg: {type: 'ref', name: 'y'}
        }
      }
    }
  })
  testEq(
    () =>
      L.getInverse(
        Lambda.ppp,
        L.get(Lambda.ppp, '(λn → λm → λx → n (m x)) (λx → x) (λy → y)')
      ),
    '(λn → λm → λx → n (m x)) (λx → x) (λy → y)'
  )
  testEq(() => L.getInverse(L.mapping(x => [[x, x], x]), 2), [2, 2])
  testEq(
    () => L.get(L.mapping([['x', L._, 'z'], ['z', L._, 'x']]), ['x', 'y', 'z']),
    ['z', 'x']
  )
  testEq(
    () =>
      L.get(L.mapping((x, z) => [{x, y: L._, z}, {z: x, y: L._, x: z}]), {
        x: 1,
        y: 3,
        z: 2
      }),
    {z: 1, x: 2}
  )
  testEq(
    () =>
      L.get(L.mapping((x, y) => [{x, y, ...L._}, {x: y, y: x, ...L._}]), {
        x: 1,
        y: 2,
        z: 3
      }),
    {x: 2, y: 1}
  )
  testEq(
    () =>
      L.getInverse(
        L.mapping((x, y) => [{x, y, ...L._}, {x: y, y: x, ...L._}]),
        {x: 1, y: 2, z: 3}
      ),
    {x: 2, y: 1}
  )
})

describe('L.mappings', () => {
  testEq(
    () =>
      L.getInverse(L.array(L.mappings([['foo', 'bar'], ['you', 'me']])), [
        'me',
        'bar'
      ]),
    ['you', 'foo']
  )
  testEq(
    () =>
      L.getInverse(
        L.array(L.mappings((x, y) => [[[x, y], {x, y}], [{x, y}, [x, y]]])),
        [['a', 'b'], {x: 1, y: 2}]
      ),
    [{x: 'a', y: 'b'}, [1, 2]]
  )
})

describe('L.conjugate', () => {
  const invertObj = L.conjugate(
    L.keyed,
    L.array(L.mapping((k, v) => [[k, v], [v, k]]))
  )
  testEq(() => L.get(invertObj, {foo: 'bar', lol: 'bal'}), {
    bar: 'foo',
    bal: 'lol'
  })
})

describe('L.applyAt', () => {
  testEq(
    () =>
      L.set([L.applyAt('msg', L.dropPrefix('foo')), 'msg'], 'Bar', {
        msg: 'foobar',
        and: 'more'
      }),
    {msg: 'fooBar', and: 'more'}
  )

  const lowerUpperIso = L.iso(
    R.cond([[R.is(String), R.toUpper]]),
    R.cond([[R.is(String), R.toLower]])
  )

  const deepKeys = L.lazy(rec =>
    L.cond(
      [R.is(Array), [L.elems, deepKeys]],
      [R.is(Object), [L.entries, L.elems, L.ifElse((_, i) => i, rec, [])]]
    )
  )

  testEq(
    () =>
      L.set([L.applyAt(deepKeys, lowerUpperIso), 'Y', 0, 'B'], -3, {
        x: [1, 'a'],
        y: [{a: 2, b: 3}]
      }),
    {x: [1, 'a'], y: [{a: 2, b: -3}]}
  )
})

describe('L.forEach', () => {
  testEq(
    () => {
      let xs = []
      L.forEach((x, i) => xs.push([x, i]), L.matches(/[ab]+/g), 'Diiba daaba!')
      return xs
    },
    [['ba', 3], ['aaba', 7]]
  )
  testEq(
    () => {
      let xs = []
      L.forEach((x, i) => xs.push([x, i]), L.elems, ['a', 'b'])
      return xs
    },
    [['a', 0], ['b', 1]]
  )
  testEq(
    () => {
      let xs = []
      L.forEach((x, i) => xs.push([x, i]), L.values, {x: 1, y: 2})
      return xs
    },
    [[1, 'x'], [2, 'y']]
  )
  testEq(
    () => {
      let xs = []
      L.forEach((x, i) => xs.push([x, i]), L.branch({y: [], x: L.elems}), {
        x: ['a', 'b', 'c'],
        y: 4
      })
      return xs
    },
    [[4, 'y'], ['a', 0], ['b', 1], ['c', 2]]
  )
})

describe('L.forEachWith', () => {
  testEq(
    () =>
      L.forEachWith(() => ({}), (o, v, k) => (o[R.toUpper(k)] = v), L.values, {
        x: 2,
        y: 1
      }),
    {X: 2, Y: 1}
  )
})

describe('L.indexed', () => {
  testEq(() => L.get(L.indexed, ['a', 'b']), [[0, 'a'], [1, 'b']])
  testEq(() => L.getInverse(L.indexed, [[0, 'a'], [1, 'b']]), ['a', 'b'])
  testEq(() => L.set(L.indexed, [], ['a', 'b']), [])
  testEq(() => L.remove(L.indexed, ['a', 'b']), undefined)
  testEq(() => L.set([L.indexed, 2], [0, 'c'], ['a', 'b']), ['c', 'b'])
  testEq(() => L.set([L.indexed, 2], [3, 'c'], ['a', 'b']), ['a', 'b', 'c'])
  testEq(() => L.remove([L.indexed, 1, 0], ['a', 'b']), ['a'])
  testEq(() => L.remove([L.indexed, 0, 1], ['a', 'b']), ['b'])
  testEq(() => L.getInverse(L.indexed, null), undefined)
})

describe('L.keyed', () => {
  testEq(() => L.get(L.keyed, {x: 4, y: 2}), [['x', 4], ['y', 2]])
  testEq(() => L.getInverse(L.keyed, [['x', 4], ['y', 2]]), {x: 4, y: 2})
  testEq(() => L.getInverse(L.keyed, [['x', 4], ['x', 3], ['y', 2]]), {
    x: 3,
    y: 2
  })
  testEq(() => L.set(L.keyed, {}, {x: 4, y: 2}), undefined)
  testEq(() => L.set(L.keyed, [], {x: 4, y: 2}), {})
  testEq(() => L.set(L.keyed, undefined, {x: 4, y: 2}), undefined)
  testEq(() => L.set([L.keyed, 2], ['z', 6], {x: 4, y: 2}), {x: 4, y: 2, z: 6})
  testEq(() => L.remove([L.keyed, 1, 0], {x: 4, y: 2}), {x: 4})
  testEq(() => L.remove([L.keyed, 0, 1], {x: 4, y: 2}), {y: 2})
  testEq(() => L.getInverse(L.keyed, null), undefined)
})

describe('L.multikeyed', () => {
  testEq(() => L.get(L.multikeyed, {x: 4, y: 2}), [['x', 4], ['y', 2]])
  testEq(() => L.get(L.multikeyed, {x: [4, 3, 2], y: 2}), [
    ['x', 4],
    ['x', 3],
    ['x', 2],
    ['y', 2]
  ])
  testEq(() => L.getInverse(L.multikeyed, [['x', 4], ['y', 2]]), {x: 4, y: 2})
  testEq(
    () => L.getInverse(L.multikeyed, [['x', 4], ['x', 3], ['y', 2], ['x', 1]]),
    {
      x: [4, 3, 1],
      y: 2
    }
  )
  testEq(() => L.set(L.multikeyed, {}, {x: 4, y: 2}), undefined)
  testEq(() => L.set(L.multikeyed, [], {x: 4, y: 2}), {})
  testEq(() => L.set(L.multikeyed, undefined, {x: 4, y: 2}), undefined)
  testEq(() => L.set([L.multikeyed, 2], ['z', 6], {x: 4, y: 2}), {
    x: 4,
    y: 2,
    z: 6
  })
  testEq(() => L.remove([L.multikeyed, 1, 0], {x: 4, y: 2}), {x: 4})
  testEq(() => L.remove([L.multikeyed, 0, 1], {x: 4, y: 2}), {y: 2})
  testEq(() => L.getInverse(L.multikeyed, null), undefined)
})

describe('L.entries', () => {
  testEq(() => L.modify(L.entries, kv => [kv[1], kv[0]], {x: 'a', y: 'b'}), {
    a: 'x',
    b: 'y'
  })
  testEq(
    () => L.remove([L.entries, 1, L.when(x => x === 'a')], {x: 'a', y: 'b'}),
    {
      y: 'b'
    }
  )
})

describe('L.querystring', () => {
  testEq(() => L.get(L.querystring, {not: 'a string'}), undefined)
  testEq(() => L.get(L.querystring, 'foo=bar+baz&abc=xyz&abc=123&corge'), {
    foo: 'bar baz',
    abc: ['xyz', '123'],
    corge: ''
  })
  testEq(
    () =>
      L.getInverse(L.querystring, {
        foo: 'bar baz',
        abc: [true, 123],
        corge: ''
      }),
    'foo=bar%20baz&abc=true&abc=123&corge'
  )
})

describe('L.keys', () => {
  testEq(() => L.modify(L.keys, R.toUpper, {x: 6, y: 9}), {X: 6, Y: 9})
  testEq(() => L.remove([L.keys, L.when(x => x > 'b')], {a: 1, c: 3, b: 2}), {
    a: 1,
    b: 2
  })
})

describe('L.keysEverywhere', () => {
  testEq(() => L.collect(L.keysEverywhere, {x1: [{y: [{z: 1}]}], x2: 2}), [
    'x1',
    'y',
    'z',
    'x2'
  ])
  testEq(() => L.modify(L.keysEverywhere, R.toUpper, {x: [{y: [{z: 1}]}]}), {
    X: [{Y: [{Z: 1}]}]
  })
  testEq(
    () =>
      L.remove([L.keysEverywhere, L.when(R.equals('y'))], {x: [{y: [{z: 1}]}]}),
    {x: [{}]}
  )
})

describe('L.reverse', () => {
  testEq(() => L.get(L.reverse, 42), undefined)
  testEq(() => L.set(L.reverse, undefined, 42), undefined)
  testEq(() => L.modify([L.reverse, L.elems], (x, i) => [x, i], [3, 1, 4]), [
    [3, 2],
    [1, 1],
    [4, 0]
  ])
  testEq(() => L.getInverse(L.reverse, null), undefined)
})

describe('L.pointer', () => {
  testEq(() => L.get(L.pointer('/f1'), {f1: 101}), 101)

  testEq(() => L.get(L.pointer(''), {a: 1, b: 2}), {a: 1, b: 2})
  testEq(() => L.get(L.pointer('/'), {'': 1, b: 2}), 1)
  testEq(() => L.get(L.pointer('/ '), {' ': 1}), 1)
  testEq(() => L.get(L.pointer('/0'), {'0': 1}), 1)
  testEq(
    () =>
      L.get(L.pointer('/a~1bc%de^fg|hi\\jk"lm~0n'), {
        'a/bc%de^fg|hi\\jk"lm~n': [1, 2]
      }),
    [1, 2]
  )
  testEq(
    () =>
      L.get(L.pointer('/a~1bc%de^fg|hi\\jk"lm~0n/0'), {
        'a/bc%de^fg|hi\\jk"lm~n': [1, 2]
      }),
    1
  )
  testEq(() => L.set(L.pointer('/b/0'), 3, {a: 1, b: [2, 3]}), {
    a: 1,
    b: [3, 3]
  })
  testEq(() => L.remove(L.pointer('/b/0'), {a: 1, b: [2, 3]}), {a: 1, b: [3]})
  testEq(() => L.modify(L.pointer('/b/0'), R.inc, {a: 1, b: [2, 3]}), {
    a: 1,
    b: [3, 3]
  })
  testEq(() => L.get(L.pointer('/-'), {'-': 101}), 101)

  testEq(() => L.get(L.pointer('#'), {a: 1, b: 2}), {a: 1, b: 2})
  testEq(() => L.get(L.pointer('#/'), {'': 1, b: 2}), 1)
  testEq(() => L.get(L.pointer('#/%20'), {' ': 1}), 1)
  testEq(() => L.get(L.pointer('#/0'), {'0': 1}), 1)
  testEq(
    () =>
      L.get(L.pointer('#/a~1bc%25de^fg%7Chi%5Cjk%22lm~0n'), {
        'a/bc%de^fg|hi\\jk"lm~n': [1, 2]
      }),
    [1, 2]
  )
  testEq(
    () =>
      L.get(L.pointer('#/a~1bc%25de^fg%7Chi%5Cjk%22lm~0n/0'), {
        'a/bc%de^fg|hi\\jk"lm~n': [1, 2]
      }),
    1
  )
  testEq(() => L.set(L.pointer('#/b/0'), 3, {a: 1, b: [2, 3]}), {
    a: 1,
    b: [3, 3]
  })
  testEq(() => L.remove(L.pointer('#/b/0'), {a: 1, b: [2, 3]}), {a: 1, b: [3]})
  testEq(() => L.modify(L.pointer('#/b/0'), R.inc, {a: 1, b: [2, 3]}), {
    a: 1,
    b: [3, 3]
  })
  testThrows(() => L.pointer('#%'))
})

describe('L.dropPrefix', () => {
  testEq(() => L.get(L.dropPrefix('foo'), 'bar'), undefined)
  testEq(() => L.get(L.dropPrefix('foo'), 'foobar'), 'bar')
  testEq(() => L.getInverse(L.dropPrefix('foo'), 'bar'), 'foobar')
  testEq(() => L.get(L.dropPrefix('foo'), ['not a string']), undefined)
  testEq(() => L.getInverse(L.dropPrefix('foo'), ['not a string']), undefined)
})

describe('L.dropSuffix', () => {
  testEq(() => L.get(L.dropSuffix('foo'), 'bar'), undefined)
  testEq(() => L.get(L.dropSuffix('bar'), 'foobar'), 'foo')
  testEq(() => L.getInverse(L.dropSuffix('bar'), 'foo'), 'foobar')
  testEq(() => L.get(L.dropSuffix('bar'), ['not a string']), undefined)
  testEq(() => L.getInverse(L.dropSuffix('bar'), ['not a string']), undefined)
})

describe('L.replaces', () => {
  testEq(() => L.get(L.replaces('+', '%20'), 'fo+ob+ar'), 'fo%20ob%20ar')
  testEq(() => L.getInverse(L.replaces('+', '%20'), 'fo%20ob%20ar'), 'fo+ob+ar')
  testEq(() => L.get(L.replaces('+', '%20'), ['not a string']), undefined)
  testEq(
    () => L.getInverse(L.replaces('+', '%20'), ['not a string']),
    undefined
  )
})

describe('L.split', () => {
  testEq(() => L.get(L.split(','), 'fo,ob,ar'), ['fo', 'ob', 'ar'])
  testEq(() => L.get(X.split(',', /,\s*/), 'fo, ob, ar'), ['fo', 'ob', 'ar'])
  testEq(() => L.getInverse(L.split(','), ['fo', 'ob', 'ar']), 'fo,ob,ar')
  testEq(() => L.get(L.split(','), ['not a', 'string']), undefined)
  testEq(() => L.getInverse(L.split(','), 'not an array'), undefined)
})

describe('L.disjoint', () => {
  testEq(
    () =>
      L.get(L.disjoint(k => (k < 'x' ? 'primary' : 'secondary')), {
        a: 'b',
        x: 1,
        y: 2
      }),
    {
      primary: {a: 'b'},
      secondary: {x: 1, y: 2}
    }
  )
  testEq(
    () =>
      L.getInverse(L.disjoint(k => (k < 'x' ? 'primary' : 'secondary')), {
        primary: {a: 'b'},
        secondary: {x: 1, y: 2}
      }),
    {a: 'b', x: 1, y: 2}
  )
  testEq(() => L.getInverse(L.disjoint(R.identity), 'not an object'), undefined)
  testEq(() => L.getInverse(L.disjoint(R.identity), {x: 'not object'}), {})
  testEq(
    () => L.getInverse(L.disjoint(R.always('g')), {x: {k: 'not in group'}}),
    {}
  )
})

describe('L.uncouple', () => {
  testEq(() => L.get(L.uncouple('='), 'foo'), ['foo', ''])
  testEq(() => L.get(L.uncouple('='), 'fo=ob=ar'), ['fo', 'ob=ar'])
  testEq(() => L.get(X.uncouple('=', '/'), 'fo/ob=ar'), ['fo', 'ob=ar'])
  testEq(() => L.get(X.uncouple('=', /\s*=\s*/), 'fo = ob = ar'), [
    'fo',
    'ob = ar'
  ])
  testEq(() => L.getInverse(L.uncouple('='), ['foo', 'bar']), 'foo=bar')
  testEq(() => L.get(L.uncouple('='), ['not a', 'string']), undefined)
  testEq(() => L.getInverse(L.uncouple('='), 'not an array'), undefined)
  testEq(() => L.getInverse(L.uncouple('='), ['foo', '']), 'foo')
})

describe('arithmetic', () => {
  testEq(() => L.modify(L.add(1), x => 1 - x, 1), -2)
  testEq(() => L.modify(L.subtract(-1), x => 1 - x, 1), -2)
  testEq(() => L.modify(L.multiply(2), x => 1 - x, 1), -0.5)
  testEq(() => L.modify(L.divide(0.5), x => 1 - x, 1), -0.5)
  testEq(() => L.modify(L.negate, x => 1 - x, 1), -2)
})

describe('L.flat', () => {
  testEq(
    () =>
      L.modify(L.flat('a', 'b', 'c'), R.negate, [
        {a: [[{b: {c: 1}}], [{b: [{c: 2}]}]]},
        {a: {b: {c: [[[3]]]}}}
      ]),
    [{a: [[{b: {c: -1}}], [{b: [{c: -2}]}]]}, {a: {b: {c: [[[-3]]]}}}]
  )
})

describe('ix', () => {
  const leavesWithKeys = L.lazy(rec =>
    L.cond(
      [R.is(Array), [L.skipIx(L.elems), rec]],
      [R.is(Object), [L.joinIx(L.values), rec]],
      [L.mapIx(L.collect(L.flatten))]
    )
  )
  testEq(
    () =>
      L.modify(leavesWithKeys, (value, keys) => ({value, keys}), {
        l1: [{l2: {l3: [1]}, l4: 2}]
      }),
    {
      l1: [
        {
          l2: {l3: [{value: 1, keys: ['l1', 'l2', 'l3']}]},
          l4: {value: 2, keys: ['l1', 'l4']}
        }
      ]
    }
  )

  testEq(
    () =>
      L.getAs(
        R.nthArg(1),
        [L.skipIx('x'), L.joinIx('y'), L.joinIx(L.setIx())],
        undefined
      ),
    'y'
  )

  const leavesWithContexts = [
    L.setIx([]),
    L.lazy(rec =>
      L.cond(
        [R.is(Array), [L.skipIx(L.elems), rec]],
        [R.is(Object), [L.mapIx((i, v) => [...i, v]), L.skipIx(L.values), rec]],
        [[]]
      )
    )
  ]
  testEq(
    () =>
      X.modify(leavesWithContexts, (value, contexts) => ({value, contexts}), {
        l1: [{l2: {l3: [1]}}]
      }),
    {
      l1: [
        {
          l2: {
            l3: [
              {
                value: 1,
                contexts: [{l1: [{l2: {l3: [1]}}]}, {l2: {l3: [1]}}, {l3: [1]}]
              }
            ]
          }
        }
      ]
    }
  )

  testEq(() => L.getAs((v, i) => [v, i], ['foo', L.setIx('bar')], {foo: 101}), [
    101,
    'bar'
  ])
  testEq(() => L.getAs(x => x + 1, x => x * 2, 3), 7)
})

describe('async', () => {
  testEq(() => L.modifyAsync(L.elems, x => later(5, -x), [3, 1, 4]), [
    -3,
    -1,
    -4
  ])

  testEq(
    () => L.transformAsync([L.elems, L.modifyOp(x => later(5, -x))], [3, 1, 4]),
    [-3, -1, -4]
  )

  testEq(async () => {
    const result = L.modifyAsync(
      L.elems,
      async _ => {
        throw Error('error')
      },
      [1]
    )
    try {
      await result
      return false
    } catch (_) {
      return result instanceof Promise
    }
  }, true)

  testThrows(() =>
    L.modifyAsync(
      L.elems,
      async _ => {
        throw Error('error')
      },
      [1]
    )
  )
})

if (process.env.NODE_ENV !== 'production') {
  describe('debug', () => {
    testThrows(() => X.set(-1, 0, 0))

    testThrows(() => X.index('x'))
    testThrows(() => X.index(-1))
    testThrows(() => X.index())

    testThrows(() => X.prop(2))
    testThrows(() => X.prop(x => x))
    testThrows(() => X.prop())

    testThrows(() => L.set(L.props('length'), 'lol', undefined))
    testThrows(() => L.set(L.slice(undefined, undefined), 11, []))
    testThrows(() => L.pick(new XYZ(1, 2, 3)))
    testThrows(() => L.set(L.filter(undefined, undefined), {x: 11}, []))

    testThrows(() => L.set(null, 1, 2))

    testThrows(() => L.toFunction((_one, _too, _many) => 1))

    testThrows(() => L.get(L.seq(0), ['x']))

    testThrows(() => L.branch(new XYZ(L.identity, L.identity, L.identity)))

    testThrows(() => L.toFunction(-1))

    testThrows(() => L.joinAs(I.id, 0))

    testThrows(() => L.cond([]))

    testThrows(() => L.cond([0, 1]))

    testThrows(() => L.mapping(x => [[...x, ...x], []]))
    testThrows(() => L.mapping(x => [[...x, {...x}], []]))
    testThrows(() => L.mapping(x => [...x, []]))
    testThrows(() => L.mapping(() => [new XYZ(), []]))
    testThrows(() => L.mapping((x, y) => [{...x, ...y}, []]))
  })
}

describe('cloning avoidance', () => {
  const testCloning = (name, o, x) =>
    it(`L.${name} avoids cloning`, () => {
      const y = X.modify(o, I.id, x)
      if (x !== y) throw Error('Not same')
      if (Object.isFrozen(x)) throw Error('Mutated input')
      if (process.env.NODE_ENV !== 'production') {
        const z = X.modify(o, x => (Number.isInteger(x) ? x + 1 : x), x)
        if (!Object.isFrozen(z)) throw Error('Does not freeze')
      }
    })
  testCloning('elems', X.elems, [1, [2], NaN])
  testCloning('flatten', X.flatten, [1, [2], NaN])
  testCloning('values', X.values, {x: 1, y: [2], z: NaN})
  testCloning('branch', X.branch({x: X.identity, y: X.elems}), {
    x: 1,
    y: [2],
    z: NaN
  })
  testCloning('branches', X.branches('x', 'z', 'y'), {x: 1, y: [2], z: NaN})
  testCloning('branchOr', X.branchOr(X.identity, {y: X.elems}), {
    x: 1,
    y: [2],
    z: NaN
  })
})

describe('L.alternatives', () => {
  const iso = L.alternatives(
    L.cross([L.add(1)]),
    L.cross([L.add(1), L.add(2)]),
    L.cross([L.add(1), L.add(2), L.add(3)])
  )
  testEq(() => L.get(iso, []), undefined)
  testEq(() => L.get(iso, [1]), [2])
  testEq(() => L.getInverse(iso, [1]), [0])
  testEq(() => L.getInverse(iso, [1, 2]), [0, 0])
  testEq(() => L.get(iso, [1, 2]), [2, 4])
})

describe('obsoleted', () => {
  testEq(() => L.select([L.elems, 'x'], [{}, {x: 101}]), 101)
  testEq(
    () => L.selectAs(x => x + 1, [L.elems, 'x', L.optional], [{}, {x: 100}]),
    101
  )
  testEq(() => L.set(L.append, 1, []), [1])
  testEq(() => L.remove(L.propsOf({x: 1}), {x: 2, y: 3}), {y: 3})
})
